|
|
@@ -11,9 +11,302 @@ export function useAgentTools(options: {
|
|
|
onUpdateContent: (content: string) => void;
|
|
|
onUpdateImages: (images: string[]) => void;
|
|
|
}) {
|
|
|
+ let currentTitle = options.title ?? "";
|
|
|
+ let currentContent = options.content ?? "";
|
|
|
+ let currentImages = [...(options.images ?? [])];
|
|
|
+
|
|
|
+ function setTitle(title: string) {
|
|
|
+ currentTitle = title;
|
|
|
+ options.onUpdateTitle(title);
|
|
|
+ }
|
|
|
+
|
|
|
+ function setContent(content: string) {
|
|
|
+ currentContent = content;
|
|
|
+ options.onUpdateContent(content);
|
|
|
+ }
|
|
|
+
|
|
|
+ function setImages(images: string[]) {
|
|
|
+ currentImages = images.map((url) => ({ url, localUrl: url }));
|
|
|
+ options.onUpdateImages(images);
|
|
|
+ }
|
|
|
+
|
|
|
+ function requireNonEmptyText(value: unknown, fieldName: string) {
|
|
|
+ if (typeof value !== "string" || value.trim() === "") {
|
|
|
+ throw new Error(`${fieldName} 不能为空`);
|
|
|
+ }
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+
|
|
|
+ function findNthIndex(text: string, target: string, occurrence = 1) {
|
|
|
+ if (!Number.isInteger(occurrence) || occurrence <= 0) {
|
|
|
+ throw new Error("occurrence 必须是大于 0 的整数");
|
|
|
+ }
|
|
|
+ let fromIndex = 0;
|
|
|
+ for (let i = 0; i < occurrence; i++) {
|
|
|
+ const index = text.indexOf(target, fromIndex);
|
|
|
+ if (index < 0) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ if (i === occurrence - 1) {
|
|
|
+ return index;
|
|
|
+ }
|
|
|
+ fromIndex = index + target.length;
|
|
|
+ }
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ function buildResult(extra: Record<string, unknown> = {}) {
|
|
|
+ return {
|
|
|
+ ok: true,
|
|
|
+ title: currentTitle,
|
|
|
+ contentLength: currentContent.length,
|
|
|
+ imageCount: currentImages.length,
|
|
|
+ ...extra,
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
function registerTools(tools: ChatToolsManager) {
|
|
|
+ tools.registerTool({
|
|
|
+ name: "article_get_state",
|
|
|
+ description: "读取当前文章标题、正文与配图信息。",
|
|
|
+ parameters: {
|
|
|
+ type: "object",
|
|
|
+ properties: {
|
|
|
+ includeContent: {
|
|
|
+ type: "boolean",
|
|
|
+ description: "是否返回正文全文。默认 true。",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ handler: (args) => {
|
|
|
+ const includeContent = args?.includeContent !== false;
|
|
|
+ return {
|
|
|
+ ok: true,
|
|
|
+ title: currentTitle,
|
|
|
+ content: includeContent ? currentContent : undefined,
|
|
|
+ contentLength: currentContent.length,
|
|
|
+ images: currentImages.map((item) => item.url),
|
|
|
+ };
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ tools.registerTool({
|
|
|
+ name: "article_set_title",
|
|
|
+ description: "设置文章标题。",
|
|
|
+ parameters: {
|
|
|
+ type: "object",
|
|
|
+ properties: {
|
|
|
+ title: {
|
|
|
+ type: "string",
|
|
|
+ description: "新的标题文本。",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ required: ["title"],
|
|
|
+ },
|
|
|
+ handler: (args) => {
|
|
|
+ const title = requireNonEmptyText(args?.title, "title");
|
|
|
+ setTitle(title);
|
|
|
+ return buildResult({ changed: "title" });
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ tools.registerTool({
|
|
|
+ name: "article_set_content",
|
|
|
+ description: "覆盖写入整篇正文,适合大改重写。",
|
|
|
+ parameters: {
|
|
|
+ type: "object",
|
|
|
+ properties: {
|
|
|
+ content: {
|
|
|
+ type: "string",
|
|
|
+ description: "新的正文全文。",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ required: ["content"],
|
|
|
+ },
|
|
|
+ handler: (args) => {
|
|
|
+ if (typeof args?.content !== "string") {
|
|
|
+ throw new Error("content 必须是字符串");
|
|
|
+ }
|
|
|
+ setContent(args.content);
|
|
|
+ return buildResult({ changed: "content" });
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ tools.registerTool({
|
|
|
+ name: "article_replace_text",
|
|
|
+ description: "在正文中替换指定文本片段。",
|
|
|
+ parameters: {
|
|
|
+ type: "object",
|
|
|
+ properties: {
|
|
|
+ target: {
|
|
|
+ type: "string",
|
|
|
+ description: "待替换的原文。",
|
|
|
+ },
|
|
|
+ replacement: {
|
|
|
+ type: "string",
|
|
|
+ description: "替换后的文本。",
|
|
|
+ },
|
|
|
+ occurrence: {
|
|
|
+ type: "number",
|
|
|
+ description: "替换第几次出现,默认 1。",
|
|
|
+ },
|
|
|
+ replaceAll: {
|
|
|
+ type: "boolean",
|
|
|
+ description: "是否替换全部出现位置,默认 false。",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ required: ["target", "replacement"],
|
|
|
+ },
|
|
|
+ handler: (args) => {
|
|
|
+ const target = requireNonEmptyText(args?.target, "target");
|
|
|
+ const replacement = typeof args?.replacement === "string" ? args.replacement : "";
|
|
|
+ const replaceAll = args?.replaceAll === true;
|
|
|
+ if (replaceAll) {
|
|
|
+ if (!currentContent.includes(target)) {
|
|
|
+ throw new Error("未找到待替换文本");
|
|
|
+ }
|
|
|
+ setContent(currentContent.split(target).join(replacement));
|
|
|
+ return buildResult({ changed: "content", mode: "replaceAll" });
|
|
|
+ }
|
|
|
+
|
|
|
+ const occurrence = typeof args?.occurrence === "number" ? args.occurrence : 1;
|
|
|
+ const index = findNthIndex(currentContent, target, occurrence);
|
|
|
+ if (index < 0) {
|
|
|
+ throw new Error("未找到指定位置的待替换文本");
|
|
|
+ }
|
|
|
+ const updated =
|
|
|
+ currentContent.slice(0, index) +
|
|
|
+ replacement +
|
|
|
+ currentContent.slice(index + target.length);
|
|
|
+ setContent(updated);
|
|
|
+ return buildResult({ changed: "content", mode: "replaceOne", occurrence });
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ tools.registerTool({
|
|
|
+ name: "article_insert_text",
|
|
|
+ description: "在正文指定位置插入文本。",
|
|
|
+ parameters: {
|
|
|
+ type: "object",
|
|
|
+ properties: {
|
|
|
+ text: {
|
|
|
+ type: "string",
|
|
|
+ description: "要插入的文本。",
|
|
|
+ },
|
|
|
+ position: {
|
|
|
+ type: "string",
|
|
|
+ enum: ["start", "end", "before", "after"],
|
|
|
+ description: "插入位置,默认 end。",
|
|
|
+ },
|
|
|
+ anchor: {
|
|
|
+ type: "string",
|
|
|
+ description: "当 position 为 before/after 时的锚点文本。",
|
|
|
+ },
|
|
|
+ occurrence: {
|
|
|
+ type: "number",
|
|
|
+ description: "锚点第几次出现,默认 1。",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ required: ["text"],
|
|
|
+ },
|
|
|
+ handler: (args) => {
|
|
|
+ const text = requireNonEmptyText(args?.text, "text");
|
|
|
+ const position = typeof args?.position === "string" ? args.position : "end";
|
|
|
+ let nextContent = currentContent;
|
|
|
+
|
|
|
+ if (position === "start") {
|
|
|
+ nextContent = `${text}${currentContent}`;
|
|
|
+ } else if (position === "end") {
|
|
|
+ nextContent = `${currentContent}${text}`;
|
|
|
+ } else {
|
|
|
+ const anchor = requireNonEmptyText(args?.anchor, "anchor");
|
|
|
+ const occurrence = typeof args?.occurrence === "number" ? args.occurrence : 1;
|
|
|
+ const index = findNthIndex(currentContent, anchor, occurrence);
|
|
|
+ if (index < 0) {
|
|
|
+ throw new Error("未找到锚点文本");
|
|
|
+ }
|
|
|
+ if (position === "before") {
|
|
|
+ nextContent = currentContent.slice(0, index) + text + currentContent.slice(index);
|
|
|
+ } else if (position === "after") {
|
|
|
+ const afterIndex = index + anchor.length;
|
|
|
+ nextContent =
|
|
|
+ currentContent.slice(0, afterIndex) + text + currentContent.slice(afterIndex);
|
|
|
+ } else {
|
|
|
+ throw new Error("position 参数不合法");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setContent(nextContent);
|
|
|
+ return buildResult({ changed: "content", action: "insert", position });
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ tools.registerTool({
|
|
|
+ name: "article_delete_text",
|
|
|
+ description: "删除正文中的指定文本片段。",
|
|
|
+ parameters: {
|
|
|
+ type: "object",
|
|
|
+ properties: {
|
|
|
+ target: {
|
|
|
+ type: "string",
|
|
|
+ description: "要删除的文本。",
|
|
|
+ },
|
|
|
+ occurrence: {
|
|
|
+ type: "number",
|
|
|
+ description: "删除第几次出现,默认 1。",
|
|
|
+ },
|
|
|
+ deleteAll: {
|
|
|
+ type: "boolean",
|
|
|
+ description: "是否删除全部出现位置,默认 false。",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ required: ["target"],
|
|
|
+ },
|
|
|
+ handler: (args) => {
|
|
|
+ const target = requireNonEmptyText(args?.target, "target");
|
|
|
+ const deleteAll = args?.deleteAll === true;
|
|
|
+ if (deleteAll) {
|
|
|
+ if (!currentContent.includes(target)) {
|
|
|
+ throw new Error("未找到待删除文本");
|
|
|
+ }
|
|
|
+ setContent(currentContent.split(target).join(""));
|
|
|
+ return buildResult({ changed: "content", mode: "deleteAll" });
|
|
|
+ }
|
|
|
+
|
|
|
+ const occurrence = typeof args?.occurrence === "number" ? args.occurrence : 1;
|
|
|
+ const index = findNthIndex(currentContent, target, occurrence);
|
|
|
+ if (index < 0) {
|
|
|
+ throw new Error("未找到指定位置的待删除文本");
|
|
|
+ }
|
|
|
+ const updated = currentContent.slice(0, index) + currentContent.slice(index + target.length);
|
|
|
+ setContent(updated);
|
|
|
+ return buildResult({ changed: "content", mode: "deleteOne", occurrence });
|
|
|
+ },
|
|
|
+ });
|
|
|
|
|
|
+ tools.registerTool({
|
|
|
+ name: "article_set_images",
|
|
|
+ description: "设置文章配图 URL 列表。",
|
|
|
+ parameters: {
|
|
|
+ type: "object",
|
|
|
+ properties: {
|
|
|
+ images: {
|
|
|
+ type: "array",
|
|
|
+ items: { type: "string" },
|
|
|
+ description: "完整的图片 URL 数组(会覆盖原有配图)。",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ required: ["images"],
|
|
|
+ },
|
|
|
+ handler: (args) => {
|
|
|
+ if (!Array.isArray(args?.images)) {
|
|
|
+ throw new Error("images 必须是字符串数组");
|
|
|
+ }
|
|
|
+ const imageUrls = args.images.filter((item: unknown) => typeof item === "string");
|
|
|
+ setImages(imageUrls);
|
|
|
+ return buildResult({ changed: "images", images: imageUrls });
|
|
|
+ },
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
return {
|