快乐的梦鱼 2 тижнів тому
батько
коміт
23d484fc15

+ 1 - 0
1.txt

@@ -0,0 +1 @@
+为我写一篇关于乡愁的简短文章,200字左右

+ 2 - 2
src/api/RequestModules.ts

@@ -25,8 +25,8 @@ export class AppServerRequestModule<T extends DataModel> extends BaseAppServerRe
  */
 export class MengyuServerRequestModule<T extends DataModel> extends BaseAppServerRequestModule<T> {
   constructor() {
-    //super('https://update-server1.imengyu.top');
-    super('http://localhost:3012');
+    super('https://update-server1.imengyu.top');
+    //super('http://localhost:3012');
     this.config.requestInterceptor = (url, req) => {
       req.headers['token'] = getApp().globalData?.token ?? '';
       url = appendGetUrlParams(url, 'identifier', this.getDeviceUid());

Різницю між файлами не показано, бо вона завелика
+ 1 - 0
src/common/style/icons.ts


+ 6 - 1
src/manifest.json

@@ -65,7 +65,12 @@
         },
         "requiredPrivateInfos" : [ "getLocation", "chooseAddress", "chooseLocation", "choosePoi" ],
         "nativeTags": [ "official-account-publish" ],
-        "plugins" : {}
+        "plugins": {
+          "WechatSI": {
+            "version": "0.3.6", 
+            "provider": "wx069ba97219f66d99"
+          }
+        }
     },
     "mp-alipay" : {
         "usingComponents" : true

+ 94 - 31
src/pages/chat/components/ChatFooter.vue

@@ -1,47 +1,110 @@
 <template>
-  <FlexRow
-    position="relative" 
-    border="1px solid #e0e0e0" 
-    :radius="30"
-    :padding="20"
-    :margin="20"
-    align="center"
-  >
-    <textarea 
-      class="input"
-      v-model="input"
-      placeholder="给智能助手提问题,交流灵感..."
-      auto-height
-      confirm-type="send"
-      style="flex: 1;"
+  <FlexCol>
+    <slot name="header" />
+    <FlexRow
+      position="relative" 
+      border="1px solid #e0e0e0" 
+      :radius="30"
+      :padding="20"
+      :margin="20"
+      align="center"
     >
-    </textarea>
-    <IconButton 
-      icon="navigation"
-      :touchable="Boolean(input)"
-      :loading="props.chatManager.isLoading.value"
-      :innerStyle="{
-        backgroundColor: '#000',
-        borderRadius: '50%',
-        padding: '10rpx',
-      }"
-      color="white"
-      @click="props.chatManager.send(input)"
-    />
-  </FlexRow>
+      <slot v-if="isSelectMode" name="mulitSelectMode" />
+      <template v-else>
+        <IconButton 
+          :icon="isVoiceMode ? 'keyboard-9' : 'voice'"
+          :innerStyle="{
+            borderRadius: '50%',
+            padding: '10rpx',
+          }"
+          @click="isVoiceMode = !isVoiceMode"
+        />
+        <view 
+          v-if="isVoiceMode"
+          :style="{
+            display: 'flex',
+            flexDirection: 'column',
+            alignItems: 'center',
+            justifyContent: 'center',
+            flex: 1,
+            background: isRecording ? '#000' : '',
+          }"
+          @touchstart="handleTouchStart"
+          @touchend="handleTouchEnd"
+        >
+          <text v-if="isRecording" style="color: #fff;">松开 发送</text>
+          <text v-else>按住 说话</text>
+        </view>
+        <textarea 
+          v-else
+          class="input"
+          v-model="input"
+          placeholder="给智能助手提问题,交流灵感..."
+          auto-height
+          confirm-type="send"
+          style="flex: 1;"
+        >
+        </textarea>
+        <IconButton 
+          icon="picture"
+          :loading="props.chatManager.isLoading.value"
+          :innerStyle="{
+            borderRadius: '50%',
+            padding: '10rpx',
+          }"
+          @click="props.chatInterfaceManager.uploadAttachment?.()"
+        />
+        <IconButton 
+          icon="navigation"
+          :touchable="Boolean(input)"
+          :loading="props.chatManager.isLoading.value"
+          :innerStyle="{
+            backgroundColor: '#000',
+            borderRadius: '50%',
+            padding: '10rpx',
+          }"
+          color="white"
+          @click="props.chatManager.send(input)"
+        />
+      </template>
+    </FlexRow>
+  </FlexCol>
 </template>
 
 <script setup lang="ts">
 import IconButton from '@/components/basic/IconButton.vue';
-import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import { ref } from 'vue';
-import type { ChatManager } from '../core/Chat';
+import type { ChatInterfaceManager, ChatManager } from '../core/Chat';
+import { useChatSelectionChild } from '../composables/useChatSelection';
+import { useChatRecordInput } from '../composables/useChatRecordInput';
+import { toast } from '@/components/utils/DialogAction';
+import FlexCol from '@/components/layout/FlexCol.vue';
 
 const props = defineProps<{
   chatManager: ChatManager;
+  chatInterfaceManager: ChatInterfaceManager;
 }>();
 
+
 const input = ref('');
 
+const isVoiceMode = ref(false);
+const { isSelectMode } = useChatSelectionChild();
+const { 
+  isRecording,
+  handleTouchEnd,
+  handleTouchStart
+} = useChatRecordInput({
+  onRecognize: (text: string) => {
+    if (text.trim()) {
+      props.chatManager.send(text);
+    } else {
+      toast('你似乎没说什么,请重新说一遍');
+    }
+  },
+  onError: (error: any) => {
+    toast('识别失败 ' + error);
+  },
+});
 </script>

+ 43 - 0
src/pages/chat/components/Footer/ChatMulitSelectBar.vue

@@ -0,0 +1,43 @@
+<template>  
+  <FlexRow align="center" gap="gap.sm">
+    <span>已选择 {{ selectedCount }} 条消息</span>
+    <FlexRow align="center" gap="gap.sm" overflow="auto">
+      <Button
+        type="danger" 
+        icon="trash"
+        :disabled="!isAnyItemSelected" 
+        @click="sessionManager.onDeleteMessages(selectedMessageIds)"
+      >
+        删除
+      </Button>
+      <Button type="primary" icon="select" @click="emit('cancel')">
+        完成
+      </Button>
+    </FlexRow>
+  </FlexRow>
+</template>
+
+<script setup lang="ts">
+import { useChatSelectionChild } from '../../composables/useChatSelection';
+import { useChatExportSave } from '../../composables/useChatExportSave';
+import type { ChatMessage } from '../../model/Message';
+import type { ChatSessionManager } from '../../composables/useChatSession';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Button from '@/components/basic/Button.vue';
+
+const props = defineProps<{
+  selectedCount: number;
+  messages: ChatMessage[];
+  sessionManager: ChatSessionManager;
+}>();
+
+const emit = defineEmits<{
+  (e: 'cancel'): void;
+  (e: 'delete'): void;
+}>();
+
+const {
+  isAnyItemSelected,
+  selectedMessageIds
+} = useChatSelectionChild();
+</script>

+ 68 - 0
src/pages/chat/composables/useChatRecordInput.ts

@@ -0,0 +1,68 @@
+import { onUnmounted, ref } from "vue"
+
+export function useChatRecordInput(options: {
+  onRecognize: (text: string) => void;
+  onError: (error: any) => void;
+}) {
+  const isRecording = ref(false)
+  const recognizedText = ref('')
+  let recordManager = null
+
+  // --- 初始化 ---
+  // #ifdef MP-WEIXIN
+  // 引入微信同声传译插件
+  const plugin = requirePlugin('WechatSI')
+  recordManager = plugin.getRecordRecognitionManager()
+  recordManager.onStart = () => {
+    console.log('录音开始')
+    isRecording.value = true
+    recognizedText.value = '' // 开始新一次识别,清空旧结果
+  }
+
+  // 监听识别结束
+  recordManager.onStop = (res: any) => {
+    console.log('录音结束,最终结果:', res)
+    isRecording.value = false
+    recognizedText.value = res.result
+    options.onRecognize(res.result)
+  }
+  // 监听错误
+  recordManager.onError = (res: any) => {
+    console.error('识别出错:', res)
+    isRecording.value = false
+    uni.showToast({ title: '识别失败', icon: 'none' })
+    options.onError(res.msg)
+  }
+  // #endif
+
+  // --- 事件处理 ---
+  const handleTouchStart = () => {
+    // #ifdef MP-WEIXIN
+    if (isRecording.value) return
+    recordManager.start({
+      duration: 30000, // 最长录音时长,单位ms
+      lang: 'zh_CN'    // 识别语言
+    })
+    // #endif
+  }
+  const handleTouchEnd = () => {
+    // #ifdef MP-WEIXIN
+    if (!isRecording.value) return
+    recordManager.stop()
+    // #endif
+  }
+
+  // --- 组件卸载清理 ---
+  onUnmounted(() => {
+    // 组件销毁时,确保停止录音,避免异常
+    if (isRecording.value && recordManager) {
+      recordManager.stop()
+    }
+  })
+
+  return {
+    isRecording,
+    handleTouchStart,
+    handleTouchEnd,
+  }
+}

+ 22 - 3
src/pages/chat/core/Chat.ts

@@ -41,6 +41,7 @@ export type ChatInterfaceManager = {
   setInputValue?: (value: string) => void;
   scrollToBottom?: () => void;
   stopMessageEditing?: () => void;
+  uploadAttachment?: () => void;
   getAttachmentList?: () => Promise<ChatAttachmentItem[]>;
 };
 export interface ChatConfig {
@@ -87,11 +88,21 @@ export interface ChatConfig {
    */
   onNewChat?: (isNewChat: boolean) => void;
   /**
+   * 消息合并前
+   * @param message - 消息
+   */
+  onBeforeMessageMerge?: (userMessages: ChatMessageModel[], currentUserMessageIds: number[]) => void;
+  /**
    * 发送消息前
    * @param message - 消息
    */
   onBeforeSend?: (userMessages: ChatMessageModel[], streamOptions: any) => void;
   /**
+   * 程序追加消息,不保存到数据库
+   * @returns 
+   */
+  onGetAppendMessages?: () => ChatMessageModel[];
+  /**
    * 对话消息结束
    * @param message - 消息
    * @param currentUserMessageIds - 当前用户消息ID列表
@@ -145,6 +156,7 @@ export function useChat(options: {
     contextMemorySetting: config.contextMemorySetting,
     contextMemoryConfig: config.contextMemoryConfig,
     sessionManager: sessionManager,
+    onGetAppendMessages: config.onGetAppendMessages,
   });
   const toolCallsManager = useToolCalls(messagesManager, toolsManager, interfaceManager, sessionManager);
 
@@ -258,6 +270,8 @@ export function useChat(options: {
     const toolCallBuffers = new Map<number, { id?: string; name?: string; arguments: string }>();
     let streamOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming;
     try {
+      config.onBeforeMessageMerge?.(messages.value.filter(m => currentUserMessageIds.includes(m.id)), currentUserMessageIds);
+
       // 用于拼接 tool_calls 的 arguments(流式会分片)
       streamOptions = await buildStreamOptions(
         sendOptions,
@@ -405,11 +419,16 @@ export function useChat(options: {
     eventSource.onerror = (error) => {
       console.error("SSE连接错误:", error);
       eventSource?.close();
-
       isLoading.value = false;
       streamingAiMessageId = null;
-      aiMessage.content = "失败,请稍后重试。";
-      aiMessage.setError("请求错误", ChatUtils.formatError(error));
+
+      if (('' +error).includes('402')) {
+        aiMessage.content = "您今日的对话次数已用完,请明日再来吧。";
+        aiMessage.state = "error";
+      } else {
+        aiMessage.content = "失败,请稍后重试。";
+        aiMessage.setError("请求错误", ChatUtils.formatError(error));
+      }
       sessionManager.persistMessages([aiMessage]);
     };
 

+ 6 - 0
src/pages/chat/core/Context.ts

@@ -66,6 +66,10 @@ export function useChatContext(options: {
    * 会话管理器
    */
   sessionManager: ChatSessionManager,
+  /**
+   * 程序追加消息,不保存到数据库
+   */
+  onGetAppendMessages?: () => ChatMessage[];
 }) {
   const contextMemorySetting = options.contextMemorySetting || 'slide-window';
   const contextMemoryConfig = {
@@ -156,6 +160,8 @@ export function useChatContext(options: {
    */
   function getRoundedMessages() {
     const messages = options.message.value;
+    if (options.onGetAppendMessages)
+      messages.push(...options.onGetAppendMessages());
     const groupedMessages: RoundedMessages[] = messages
       .filter(m => m.parentId === 0 && m.isUser)
       .map(m => ({

+ 3 - 2
src/pages/chat/core/speical/ssemp.ts

@@ -196,7 +196,9 @@ export class SSE {
       enableChunked: true,
       success: (res) => {
         if (res.statusCode !== 200) {
-          this.emitError(new Error(`请求失败: ${res.statusCode}`));
+          this.emitError(new Error(
+            res.data ? JSON.stringify(res.data) : `请求失败: ${res.statusCode}`
+          ));
           return;
         }
         if (this.closed) {
@@ -216,7 +218,6 @@ export class SSE {
     const chunkableTask = this.requestTask as UniApp.RequestTask & {
       onChunkReceived?: (callback: (result: { data: ArrayBuffer }) => void) => void;
     };
-    console.log('chunkableTask', chunkableTask);
     if (typeof chunkableTask.onChunkReceived === "function") {
       chunkableTask.onChunkReceived((result: { data: ArrayBuffer }) => {
         if (this.closed) {

+ 47 - 11
src/pages/home/post/agent.vue

@@ -25,9 +25,17 @@
       />
     </template>
     <template #footer>
-      <ChatFooter 
+      <ChatFooter
         :chatManager="chatManager" 
-      />
+        :chatInterfaceManager="interfaceManager"
+      >
+        <template #mulitSelectMode>
+          <ChatMulitSelectBar :selectedCount="selectedCount" :messages="messages" :sessionManager="sessionManager" />
+        </template>
+        <template #header>
+
+        </template>
+      </ChatFooter>
     </template>
   </ChatMessageContainer>
 </template>
@@ -46,6 +54,7 @@ import ChatMessageContainer from '../../chat/components/ChatMessageContainer.vue
 import NavBar from '@/components/nav/NavBar.vue';
 import ChatSessionSidebar from '@/pages/chat/components/Session/ChatSessionSidebar.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
+import ChatMulitSelectBar from '@/pages/chat/components/Footer/ChatMulitSelectBar.vue';
 
 const messages = ref<ChatMessageModel[]>([]) as Ref<ChatMessageModel[]>;
 const interfaceManager: ChatInterfaceManager = {
@@ -53,6 +62,9 @@ const interfaceManager: ChatInterfaceManager = {
   getAttachmentList: async () => {
     return [];
   },
+  uploadAttachment() {
+    emit('upload');
+  },
 };
 
 const props = defineProps<{
@@ -67,10 +79,11 @@ const emit = defineEmits([
   'update:title',
   'update:content',
   'update:images',
-  'close'
+  'close',
+  'upload'
 ]);
 
-const showSessionSidebar = ref(true);
+const showSessionSidebar = ref(false);
 
 const sessionManager = useChatSession({
   messages,
@@ -93,6 +106,32 @@ const { registerTools } = useAgentTools({
   onUpdateImages: (images) => emit('update:images', images),
 });
 
+const welcomeActions = [
+  '乡村振兴的现状与未来', 
+  '乡源情怀是什么',
+  '乡源文化的传承与发展',
+  '乡源文化的保护与利用',
+  '乡源文化的传承与发展',
+  '乡源文化的保护与利用',
+  '帮我续写这篇笔记',
+  '帮我润色这篇笔记',
+  '帮我改写这篇笔记的开头',
+  '帮我提供几个更吸引人的标题',
+  '总结一下这篇笔记的要点',
+  '给这段话添加相关案例或数据',
+  '检查并修正文中的错别字和语病',
+  '根据当前内容延伸新的段落',
+  '将这篇笔记缩写成200字以内',
+  '把文章改写得更具故事性',
+  '帮我检查逻辑和表达是否清晰',
+  '请提出内容改进的建议',
+  '根据现有内容给出分段和小标题',
+  '尝试用不同文风重写这段:略带幽默或更正式',
+  '请帮我删除冗余和重复内容',
+  '模仿范文风格改写这段文字',
+  '将内容转为适合公众号发布的结构',
+]
+
 const chatManager = useChat({
   interfaceManager,
   sessionManager,
@@ -114,7 +153,7 @@ const chatManager = useChat({
       },
       customSystemPrompt: '',
     }),
-    defaultSystemPrompt: `你是一个“笔记/文章写作与改稿助手”,工作在一个笔记编辑器里。你需要在与用户对话的同时,能直接读取并修改当前文章(标题与正文)。
+    defaultSystemPrompt: `你是一个“乡村文化挖掘笔记/文章写作与改稿助手”,工作在一个笔记编辑器里。你需要在与用户对话的同时,能直接读取并修改当前文章(标题与正文)。
 
 ## 工作目标
 - 帮用户写作:构思、列提纲、扩写、续写、改写、润色、降重、统一文风、改错别字与语病。
@@ -125,18 +164,15 @@ const chatManager = useChat({
 - 当需要改动文章时:优先使用“片段替换/插入/删除”等精确编辑工具;只有在大改(重写/大幅重排)时才使用整篇覆盖写入。
 - 任何工具写入后,都要简短说明你改了什么(1-3 条),避免长篇复述全文。
 - 如果用户只是在聊天,不需要动文章,就不要调用写入工具。
+- 用户大部分情况下需要简短的文章,请尽量在200~800字以内完成任务。
 
 ## 输出偏好
 - 先给结论/方案,再给要点;结构清晰,使用小标题与列表。
 - 涉及改稿时,优先给“可直接应用”的修改结果(通过工具写入完成),必要时补充原因。`,
     onBuildWelcome: () => {
       return {
-        welcomeMessage: '你好!欢迎使用梦鱼的写作助手。可以与我讨论写作灵感、写作计划、交代我写作任务等。',
-        welcomeActions: [
-          '为我生成一个写作计划', 
-          '为我总结一下这篇笔记主要内容', 
-          '帮我修正这篇笔记的语法错误',
-        ],
+        welcomeMessage: '你好!欢迎使用亮乡源AI伴写助手。可以与我讨论写作灵感、写作计划、交代我写作任务等。',
+        welcomeActions: [...welcomeActions].sort(() => Math.random() - 0.5).slice(0, 5),
       }
     },
     onInitTools: (toolsManager) => {

+ 293 - 0
src/pages/home/post/composables/agentTools.ts

@@ -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 {

+ 14 - 2
src/pages/home/post/publish.vue

@@ -32,8 +32,19 @@
               :upload="uploadImage"
               @updateList="onUpdateList"
             />
-            <Field v-model="title" type="text" placeholder="请输入标题" :maxLength="30" showWordLimit bac />
-            <Field v-model="content" type="text" multiline placeholder="请输入内容" :maxLength="1000" rows="10" showWordLimit />
+            <Field v-model="title" type="text" placeholder="输入标题(可选)" :maxLength="30" showWordLimit bac />
+            <Field 
+              v-model="content" 
+              type="text" 
+              multiline 
+              placeholder="请输入内容(可选)" 
+              :maxLength="1000" 
+              rows="20" 
+              :inputStyle="{
+                height: '600rpx',
+              }"
+              showWordLimit 
+            />
           </BackgroundBox>
         </ProvideVar>
         <Height :height="50" />
@@ -74,6 +85,7 @@
         v-model:title="title"
         v-model:content="content"
         v-model:images="images"
+        @upload="uploader?.pick()"
         @close="showAgentPopup = false" 
       />
     </Popup>