zen-gitsync 2.18.6 → 2.18.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -1
- package/src/config.js +41 -1
- package/src/ui/public/assets/AgentView-BRAUZCtA.js +1 -0
- package/src/ui/public/assets/{AgentView-am93H7C2.css → AgentView-DZx3n-So.css} +1 -1
- package/src/ui/public/assets/{AppVersionBadge-D4YCHNS8.js → AppVersionBadge-Bg5lrtKb.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-CpYCgEuj.js → BranchSelector-CxjTqAkl.js} +1 -1
- package/src/ui/public/assets/{CommitForm-CwOF_1gO.js → CommitForm-BWWrOvj5.js} +1 -1
- package/src/ui/public/assets/{CommonDialog-BUuLsgNS.js → CommonDialog-x5jfL4Lq.js} +1 -1
- package/src/ui/public/assets/{CopySessionButton-SgJr0xOC.js → CopySessionButton-D0wFpk8a.js} +1 -1
- package/src/ui/public/assets/{EditorView-DwbVdSmS.js → EditorView-DmaGiI-z.js} +1 -1
- package/src/ui/public/assets/{FlowExecutionViewer-CKHp0Hfi.js → FlowExecutionViewer-BkZ20jxN.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-RgXtsQ7y.js → FlowOrchestrationWorkspace-aZBYu29g.js} +1 -1
- package/src/ui/public/assets/{LogList-DdbyFS8j.js → LogList-CSIbmUMK.js} +1 -1
- package/src/ui/public/assets/{MindmapView-ep2o8uXg.js → MindmapView-JYLp185X.js} +1 -1
- package/src/ui/public/assets/{MonitorView-BTNWi4QG.js → MonitorView-BS2Mh-I7.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-DWTFof63.js → ProjectStartupButton-DAEP6ZU5.js} +1 -1
- package/src/ui/public/assets/{RecentDirectoriesChat-Do1spWVH.js → RecentDirectoriesChat-CVFTShrU.js} +1 -1
- package/src/ui/public/assets/{RemoteManagerDialog-DjLVltnm.js → RemoteManagerDialog-aaNmsKpy.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-DpcEDqyo.js → RemoteRepoCard-DndI8rNZ.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-Djj6QLgu.js → SourceMapView-BTD9-R8t.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-Du5MOe8A.js → SvgIcon-0n6uuKFm.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-CQWY6Jwx.js → UserInputNode-B-mDg9Wy.js} +1 -1
- package/src/ui/public/assets/WorkbenchView-CfSrCIbp.js +8 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-CDrhhAKC.js → _plugin-vue_export-helper-C3o7kvRL.js} +2 -2
- package/src/ui/public/assets/{agentConversations-BvuyjfgF.js → agentConversations-DpiGplZB.js} +1 -1
- package/src/ui/public/assets/configStore-BdL3uW2E.js +1 -0
- package/src/ui/public/assets/index-BS7BDFG4.js +57 -0
- package/src/ui/public/assets/index-DwJWiCiN.css +1 -0
- package/src/ui/public/assets/useOrchestrator-CS9sn7nV.js +1 -0
- package/src/ui/public/index.html +6 -6
- package/src/ui/server/routes/config.js +13 -0
- package/src/ui/server/routes/workbench/agentChat.js +43 -11
- package/src/ui/server/routes/workbench/agentChatShared.test.js +148 -57
- package/src/ui/server/routes/workbench/agentRoutes.js +23 -1
- package/src/ui/server/routes/workbench/chatPrompts.js +115 -0
- package/src/ui/server/routes/workbench/chatPrompts.test.js +114 -0
- package/src/ui/server/utils/directoryFetch.js +14 -1
- package/src/ui/server/utils/directoryFetch.test.js +5 -0
- package/src/ui/public/assets/AgentView-D-k_LYiJ.js +0 -1
- package/src/ui/public/assets/WorkbenchView-CQ8qLTMM.js +0 -8
- package/src/ui/public/assets/configStore-DojhW1P7.js +0 -1
- package/src/ui/public/assets/index-BuFKNHju.css +0 -1
- package/src/ui/public/assets/index-Z7rlN9LU.js +0 -57
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./rolldown-runtime-C0FnF6B9.js";import{i as e,mr as t,xr as n}from"./element-plus-J45pKUrE.js";import{n as r}from"./_plugin-vue_export-helper-C3o7kvRL.js";import{t as i}from"./configStore-BdL3uW2E.js";import{m as a}from"./index-BS7BDFG4.js";var o=[0,5,10,15,30,60].map(e=>e*60*1e3),s=6e5;function c(e){let t=Number(e);return o.includes(t)?t:s}function l(e){return e?e>=36e5?`@WORKBENCH:每 {n} 小时报告`:`@WORKBENCH:每 {n} 分钟报告`:`@WORKBENCH:关闭自动报告`}function u(e){return e?{n:Math.round(e>=36e5?e/36e5:e/6e4)}:{}}function d(e){switch(e){case`NO_MODEL`:return`@WORKBENCH:没有可用的 AI 模型,只记录了任务事实`;case`LLM_TIMEOUT`:return`@WORKBENCH:生成超时,只记录了任务事实`;case`LLM_EMPTY`:return`@WORKBENCH:模型没有返回正文,只记录了任务事实`;case`LLM_FAILED`:return`@WORKBENCH:生成失败,只记录了任务事实`;default:return`@WORKBENCH:生成失败,只记录了任务事实`}}function f(e){if(e==null||e===``)return null;let t=Number(e);if(!Number.isFinite(t))return null;let n=Math.round(t);return n>=0&&n<=100?n:null}function p(e){return typeof e!=`number`||!Number.isFinite(e)||e<=0?null:Math.floor(e)}var m=`@WORKBENCH:AI 估计`,h=`@WORKBENCH:百分比是主 Agent 根据任务思考与工具调用推测的大致估计,不是精确进度`;t();var g=1e5;function _(){let t=n(!0),o=n([]),l=n([]),u=n(g),d=n([]),f=n(null),p=n(!1),m=n(!1),h=n(!1),_=n(``),v=n({}),y=n([]),b=n(!1),x=n(!1),S=n(s);async function C(n=!1){try{let i=await fetch(`/api/workbench/orchestrator`,{cache:`no-store`}).then(e=>e.json());return i?.success?(t.value=i.active!==!1,Number.isInteger(i.maxInstructionChars)&&i.maxInstructionChars>0&&(u.value=i.maxInstructionChars),o.value=Array.isArray(i.instructions)?i.instructions:[],l.value=Array.isArray(i.activity)?i.activity:[],d.value=Array.isArray(i.running)?i.running:[],a().setRunning(d.value.length),f.value=i.updatedAt||null,_.value=typeof i.defaultPrompt==`string`?i.defaultPrompt:``,v.value=i.projectPrompts&&typeof i.projectPrompts==`object`?i.projectPrompts:{},S.value=c(i.reportIntervalMs),p.value=!0,!0):(n||e.error(i?.error||r(`@WORKBENCH:读取编排状态失败`)),!1)}catch(t){return n||e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),!1}}async function w(t=!1){if(b.value)return!1;b.value=!0;try{let n=await fetch(`/api/workbench/orchestrator/reports`,{cache:`no-store`}).then(e=>e.json());return n?.success?(y.value=Array.isArray(n.reports)?n.reports:[],!0):(t||e.error(n?.error||r(`@WORKBENCH:读取进度报告失败`)),!1)}catch(n){return t||e.error(r(`@WORKBENCH:网络错误: `)+(n?.message||n)),!1}finally{b.value=!1}}async function T(){if(x.value)return null;x.value=!0;try{let t=String(i().locale||``).startsWith(`en`)?`en`:`zh`,n=await fetch(`/api/workbench/orchestrator/report`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({locale:t})}).then(e=>e.json());return n?.success?n.report?(y.value=[n.report,...y.value].slice(0,20),n.report):(e.info(r(`@WORKBENCH:当前没有正在执行的任务,没有可汇报的进度`)),null):(e.error(n?.error||r(`@WORKBENCH:生成进度报告失败`)),null)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),null}finally{x.value=!1}}async function E(t){try{let n=await fetch(`/api/workbench/orchestrator/report-interval`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({intervalMs:t})}).then(e=>e.json());return n?.success?(S.value=c(n.reportIntervalMs),!0):(e.error(n?.error||r(`@WORKBENCH:保存失败`)),!1)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),!1}}async function D(t){try{let n=await fetch(`/api/workbench/orchestrator/default-prompt`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({prompt:t})}).then(e=>e.json());return n?.success?(_.value=typeof n.defaultPrompt==`string`?n.defaultPrompt:``,e.success(r(`@WORKBENCH:已保存全局预设提示词`)),!0):(e.error(n?.error||r(`@WORKBENCH:保存失败`)),!1)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),!1}}async function O(t,n){try{let i=await fetch(`/api/workbench/orchestrator/project-prompt`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({projectPath:t,prompt:n})}).then(e=>e.json());return i?.success?(v.value=i.projectPrompts&&typeof i.projectPrompts==`object`?i.projectPrompts:{},e.success(r(`@WORKBENCH:已保存项目预设提示词`)),!0):(e.error(i?.error||r(`@WORKBENCH:保存失败`)),!1)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),!1}}async function k(n){if(m.value)return!1;let i=t.value;t.value=n,m.value=!0;try{let a=await fetch(`/api/workbench/orchestrator/state`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({active:n})}).then(e=>e.json());return a?.success?(t.value=a.active!==!1,e.success(t.value?r(`@WORKBENCH:已恢复调度`):r(`@WORKBENCH:已暂停调度,派发将只建任务不执行`)),!0):(t.value=i,e.error(a?.error||r(`@WORKBENCH:切换调度状态失败`)),!1)}catch(n){return t.value=i,e.error(r(`@WORKBENCH:网络错误: `)+(n?.message||n)),!1}finally{m.value=!1}}async function A(t){if(h.value)return null;h.value=!0;try{let n=await fetch(`/api/workbench/orchestrator/dispatch`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({text:t.text,projectPath:t.projectPath||``,autoRun:t.autoRun!==!1,useDefaultPrompt:t.useDefaultPrompt!==!1,executor:t.executor,attachments:(t.attachments||[]).map(e=>({id:e.id,ext:e.ext,originalName:e.originalName}))})}).then(e=>e.json());return n?.success?(await C(!0),{task:n.task||null,ran:!!n.ran}):(e.error(n?.error||r(`@WORKBENCH:派发失败`)),null)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),null}finally{h.value=!1}}return{active:t,instructions:o,activity:l,running:d,updatedAt:f,loaded:p,togglingSchedule:m,dispatching:h,maxInstructionChars:u,defaultPrompt:_,projectPrompts:v,reports:y,loadingReports:b,generatingReport:x,reportIntervalMs:S,loadOrchestrator:C,setSchedulingActive:k,dispatch:A,saveDefaultPrompt:D,saveProjectPrompt:O,loadReports:w,generateReport:T,setReportInterval:E}}export{d as a,f as c,m as i,p as l,o as n,l as o,h as r,u as s,_ as t};
|
package/src/ui/public/index.html
CHANGED
|
@@ -10,18 +10,18 @@
|
|
|
10
10
|
<meta name="theme-color" content="#141820" media="(prefers-color-scheme: dark)" />
|
|
11
11
|
<title>Zen GitSync</title>
|
|
12
12
|
<!-- 字体改为自托管(@font-face 见 src/main.css),不再依赖 Google Fonts 外链 -->
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-BS7BDFG4.js"></script>
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/dagre-DPkLmI0C.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/element-plus-J45pKUrE.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="/assets/vendor-B3NyhlYR.js">
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-C3o7kvRL.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-0n6uuKFm.js">
|
|
20
20
|
<link rel="modulepreload" crossorigin href="/assets/socket-io-TBtw0msS.js">
|
|
21
|
-
<link rel="modulepreload" crossorigin href="/assets/configStore-
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/assets/configStore-BdL3uW2E.js">
|
|
22
22
|
<link rel="modulepreload" crossorigin href="/assets/monaco-ChZ0d0qB.js">
|
|
23
23
|
<link rel="modulepreload" crossorigin href="/assets/flow-mindmap-DvB74LE1.js">
|
|
24
|
-
<link rel="modulepreload" crossorigin href="/assets/CommonDialog-
|
|
24
|
+
<link rel="modulepreload" crossorigin href="/assets/CommonDialog-x5jfL4Lq.js">
|
|
25
25
|
<link rel="modulepreload" crossorigin href="/assets/vue-flow-8aIpM5CU.js">
|
|
26
26
|
<link rel="stylesheet" crossorigin href="/assets/element-plus-D-6gla1z.css">
|
|
27
27
|
<link rel="stylesheet" crossorigin href="/assets/vendor-Bd8DyOF9.css">
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<link rel="stylesheet" crossorigin href="/assets/flow-mindmap-DqCKZUlr.css">
|
|
31
31
|
<link rel="stylesheet" crossorigin href="/assets/CommonDialog-Bzi5ISqT.css">
|
|
32
32
|
<link rel="stylesheet" crossorigin href="/assets/vue-flow-CavUTEzp.css">
|
|
33
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
33
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DwJWiCiN.css">
|
|
34
34
|
</head>
|
|
35
35
|
<body>
|
|
36
36
|
<div id="app"></div>
|
|
@@ -1138,6 +1138,19 @@ export function registerConfigRoutes({
|
|
|
1138
1138
|
}
|
|
1139
1139
|
updates.agentPresetPrompts = normalized
|
|
1140
1140
|
}
|
|
1141
|
+
// g ai 智能体的默认提示词(全局配置):和 g ai 对话时每轮拼进系统提示词。
|
|
1142
|
+
// **空串是合法值**(= 清除),所以判空用 !== undefined;非字符串整体拒绝(400),
|
|
1143
|
+
// 与 normalizeAgentDefaultPrompt 的语义一致。
|
|
1144
|
+
if (body.agentDefaultPrompt !== undefined) {
|
|
1145
|
+
const normalized = configManager.normalizeAgentDefaultPrompt(body.agentDefaultPrompt)
|
|
1146
|
+
if (normalized === null) {
|
|
1147
|
+
return res.status(400).json({
|
|
1148
|
+
success: false,
|
|
1149
|
+
error: 'agentDefaultPrompt 非法: 需要字符串'
|
|
1150
|
+
})
|
|
1151
|
+
}
|
|
1152
|
+
updates.agentDefaultPrompt = normalized
|
|
1153
|
+
}
|
|
1141
1154
|
if (Object.keys(updates).length === 0) {
|
|
1142
1155
|
return res.status(400).json({ success: false, error: '缺少可保存的参数' })
|
|
1143
1156
|
}
|
|
@@ -59,6 +59,9 @@ import { streamChatOnce } from '../../../../cli/ai/transport.js';
|
|
|
59
59
|
import { checkDangerousCommand } from '../../../../cli/ai/safety.js';
|
|
60
60
|
import { guardCommand } from '../../../../cli/ai/platformGuard.js';
|
|
61
61
|
import configManager from '../../../../config.js';
|
|
62
|
+
// 用户固定提示词(g ai 默认提示词 + 派发提示词)的取数与拼装。与外部 CLI 引擎那条
|
|
63
|
+
// 链路共用同一份(见 runExternalTurn 的调用点),免得两块提示词在两处各拼一遍。
|
|
64
|
+
import { resolveChatPromptSection } from './chatPrompts.js';
|
|
62
65
|
|
|
63
66
|
// 单轮工具调用循环数的兜底值(防失控);实际值取全局配置 aiMaxToolIterations,
|
|
64
67
|
// 与 CLI 侧 src/cli/ai/agent.js 共用同一个配置项。
|
|
@@ -342,6 +345,10 @@ ${isWin ? `- This is Windows. The following Unix commands do NOT exist here:
|
|
|
342
345
|
// - requestBudget: { maxChars, maxMessages, maxUserChars } 每轮请求的上下文预算
|
|
343
346
|
// (解析自全局配置 aiMaxRequestChars,见 cli/ai/context.js 的 resolveRequestBudget)。
|
|
344
347
|
// 不传时 prepareRequestMessages 走默认值,行为与改造前一致。
|
|
348
|
+
// - promptSection: 用户固定提示词(g ai 默认提示词 + 派发提示词,见 chatPrompts.js)。
|
|
349
|
+
// **不传(undefined)= 自己现读**:这样定时任务那条链路(scheduler.js 直接调
|
|
350
|
+
// 本函数)不必重复接一次线。显式传 '' = 本轮确定不带(调用方已经解析过)。
|
|
351
|
+
// 只进请求副本,不落 session.messages —— 改设置立刻生效,历史会话不被腌进旧规则。
|
|
345
352
|
// - resume: true 时**不再压入 user 消息**,直接拿着会话现有的尾巴继续跑。
|
|
346
353
|
// 用于"重试上一轮":失败那轮留下的 user + 工具结果都还在,再发一遍 user
|
|
347
354
|
// 会让模型看到两条同样的请求 + 已经跑完的工具结果。
|
|
@@ -350,7 +357,7 @@ ${isWin ? `- This is Windows. The following Unix commands do NOT exist here:
|
|
|
350
357
|
// 生产调用点一律不传 —— 传了就等于换了一份传输实现。
|
|
351
358
|
//
|
|
352
359
|
// 返回: { aborted: boolean }
|
|
353
|
-
export async function runAgentTurn({ session, model, userMessage, images = [], cwd, locale, openFilePath, attachments = [], dirStatusBlock = '', signal, send, onChild, askUser, listProjects, dispatchTask, getContextBlock, requestBudget = null, resume = false, streamFn = streamChatOnce }) {
|
|
360
|
+
export async function runAgentTurn({ session, model, userMessage, images = [], cwd, locale, openFilePath, attachments = [], dirStatusBlock = '', signal, send, onChild, askUser, listProjects, dispatchTask, getContextBlock, requestBudget = null, resume = false, streamFn = streamChatOnce, promptSection }) {
|
|
354
361
|
const ctx = { cwd, locale, onChild, askUser, listProjects, dispatchTask };
|
|
355
362
|
|
|
356
363
|
// 确保 session.messages 存在
|
|
@@ -400,6 +407,21 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
|
|
|
400
407
|
|
|
401
408
|
const maxIterations = await resolveMaxToolIterations();
|
|
402
409
|
|
|
410
|
+
// 用户固定提示词(g ai 默认提示词 + 派发提示词):整轮只解析一次 —— 和快照同理,
|
|
411
|
+
// 它在这一轮里不会变,而工具循环每轮都要重拼一次请求。
|
|
412
|
+
// 调用方已经解析过(agentRoutes 为了外部引擎那条链路先解了一次)时不重复读文件;
|
|
413
|
+
// 显式传 '' 同样尊重 —— 那是"确定没有",不是"没传"。
|
|
414
|
+
let userPromptSection = promptSection;
|
|
415
|
+
if (userPromptSection === undefined) {
|
|
416
|
+
try {
|
|
417
|
+
userPromptSection = await resolveChatPromptSection({ cwd, locale });
|
|
418
|
+
} catch (err) {
|
|
419
|
+
// 取不到就当没配:这是锦上添花,不能因为它挂掉就让用户这条消息发不出去
|
|
420
|
+
logger.warn(`[agentChat] 取用户固定提示词失败,本轮跳过注入: ${err?.message || err}`);
|
|
421
|
+
userPromptSection = '';
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
403
425
|
// 预算缺省时在**这里**解析一次,与 prepareRequestMessages 的派生口径一致
|
|
404
426
|
// (不能让它自己兜默认:那样 measureContextUsage 拿到的分母会和实际裁剪用的分母
|
|
405
427
|
// 可能不同,UI 上的进度条就会说谎)。
|
|
@@ -411,9 +433,9 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
|
|
|
411
433
|
// 只作用于副本,session.messages 保持完整(与 CLI 的磁盘口径一致)。
|
|
412
434
|
// 预算来自 requestBudget(全局配置 aiMaxRequestTokens 的解析结果)。
|
|
413
435
|
const messages = prepareRequestMessages(session.messages, { locale, ...budget });
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
injectRequestContext(messages, { cwd, openFilePath, attachments, locale, workspaceBlock, dirStatusBlock });
|
|
436
|
+
// 请求级上下文:用户固定提示词 + 工作区状态快照 + 常用目录状态 + 当前打开的文档
|
|
437
|
+
// + 本轮附件路径(只改副本,不落 session.messages,下一轮不重复累积)
|
|
438
|
+
injectRequestContext(messages, { cwd, openFilePath, attachments, locale, workspaceBlock, dirStatusBlock, promptSection: userPromptSection });
|
|
417
439
|
|
|
418
440
|
// 上下文占用:在**请求发出去之前**量一次,发给 UI 画圆环。
|
|
419
441
|
// 为什么必须在发之前:provider 的真实 usage(input_tokens)要等响应回来才有,
|
|
@@ -584,21 +606,23 @@ function summarizeArgs(name, args) {
|
|
|
584
606
|
}
|
|
585
607
|
}
|
|
586
608
|
|
|
587
|
-
// ──
|
|
588
|
-
// 把"工作区各板块的状态摘要""常用目录那批目录的 Git 状态"
|
|
589
|
-
// "本轮附件的落盘路径"追加到**请求副本**的 system
|
|
590
|
-
// session.messages 与磁盘历史保持原样,下一轮也不会重复累积。
|
|
609
|
+
// ── 请求级上下文注入(用户固定提示词 / 工作区状态 / 常用目录状态 / 文件空间对话 / 本轮附件) ──
|
|
610
|
+
// 把"用户配的两块固定提示词""工作区各板块的状态摘要""常用目录那批目录的 Git 状态"
|
|
611
|
+
// "用户当前打开的文件"与"本轮附件的落盘路径"追加到**请求副本**的 system 消息末尾:
|
|
612
|
+
// 只影响这一次请求,session.messages 与磁盘历史保持原样,下一轮也不会重复累积。
|
|
591
613
|
//
|
|
592
614
|
// ⚠️ 工作区快照**必须走这条副本路径,不能塞进 session.messages 里那条 system 消息**。
|
|
593
615
|
// 那条只在首轮 push 一次(见上面 `session.messages.length === 0` 的判断)并会落盘,
|
|
594
616
|
// 快照进去就等于永久停在"会话创建那天"——git 分支、任务进度全会是过期的,
|
|
595
617
|
// 而且这种错不会报错、只会让模型理直气壮地给出错答案。有单测钉住这一点。
|
|
596
618
|
// 常用目录状态同理,而且更严重:它说的是"这一刻"的领先/落后,几小时后必然不同。
|
|
619
|
+
// 用户固定提示词同理,但理由换一条:**改设置要立刻生效**,而且它属于"每轮都在的规则",
|
|
620
|
+
// 腌进历史会话等于让用户回看时看到一份过期规则(详见 chatPrompts.js 文件头)。
|
|
597
621
|
//
|
|
598
622
|
// 附件为什么只给路径、不给内容:见 utils/agentAttachments.js 的头注释 —— 非图片附件
|
|
599
623
|
// 由服务端落盘,模型自己用 read / grep 按需取,比把几百 KB 文本内联进消息省得多。
|
|
600
624
|
// 快照块同理,只给摘要与目录路径,板块正文由模型按需读。
|
|
601
|
-
export function injectRequestContext(messages, { cwd, openFilePath, attachments = [], locale, workspaceBlock = '', dirStatusBlock = '' }) {
|
|
625
|
+
export function injectRequestContext(messages, { cwd, openFilePath, attachments = [], locale, workspaceBlock = '', dirStatusBlock = '', promptSection = '' }) {
|
|
602
626
|
if (!Array.isArray(messages)) return;
|
|
603
627
|
const en = String(locale || '').startsWith('en');
|
|
604
628
|
|
|
@@ -644,9 +668,17 @@ export function injectRequestContext(messages, { cwd, openFilePath, attachments
|
|
|
644
668
|
: `用户本轮附带了 ${files.length} 个文件,已保存到以下绝对路径:\n${lines.join('\n')}\n需要时用工具读取(内容没有内联在这里),不要臆测。`);
|
|
645
669
|
}
|
|
646
670
|
|
|
647
|
-
if (parts.length === 0) return;
|
|
671
|
+
if (parts.length === 0 && !(typeof promptSection === 'string' && promptSection.trim())) return;
|
|
648
672
|
|
|
649
|
-
|
|
673
|
+
// 用户固定提示词单独一节,且排在"当前上下文"**前面**:它是每轮都成立的规则,
|
|
674
|
+
// 而当前上下文是这一刻的快照 —— 规则在前、现场在后,读起来才是"先约束后事实"。
|
|
675
|
+
let note = '';
|
|
676
|
+
if (typeof promptSection === 'string' && promptSection.trim()) {
|
|
677
|
+
note += `\n\n# ${en ? 'Standing instructions' : '用户固定提示词'}\n${promptSection.trim()}`;
|
|
678
|
+
}
|
|
679
|
+
if (parts.length > 0) {
|
|
680
|
+
note += `\n\n# ${en ? 'Current context' : '当前上下文'}\n${parts.join('\n\n')}`;
|
|
681
|
+
}
|
|
650
682
|
const sys = messages.find(m => m && m.role === 'system' && typeof m.content === 'string');
|
|
651
683
|
if (sys) sys.content += note;
|
|
652
684
|
else messages.unshift({ role: 'system', content: note.trim() });
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// configManager / agentSessionStore 会去读真实的 ~/.zen-gitsync。
|
|
13
13
|
import { test } from 'node:test'
|
|
14
14
|
import assert from 'node:assert/strict'
|
|
15
|
-
import { mkdtempSync } from 'node:fs'
|
|
15
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs'
|
|
16
16
|
import os from 'node:os'
|
|
17
17
|
import path from 'node:path'
|
|
18
18
|
|
|
@@ -61,7 +61,7 @@ const longSession = () => {
|
|
|
61
61
|
// streamFn:传输层现在默认会自动重试(最多 10 次、指数退避,见 cli/ai/transport.js)。
|
|
62
62
|
// 这一组用例关心的是"失败时怎么收场",不是"重试几次" —— 真等退避会让一个用例跑到分钟级。
|
|
63
63
|
// 所以统一注入 maxRetries: 0 的那一份;重试自身的口径在 cli/ai/runtime.test.js 里逐条钉。
|
|
64
|
-
async function runWeb({ session, userMessage, respond, locale = 'zh-CN', signal, onEvent, resume = false, streamFn }) {
|
|
64
|
+
async function runWeb({ session, userMessage, respond, locale = 'zh-CN', signal, onEvent, resume = false, streamFn, promptSection }) {
|
|
65
65
|
const { runAgentTurn } = await import('./agentChat.js')
|
|
66
66
|
const { streamChatOnce } = await import('../../../../cli/ai/transport.js')
|
|
67
67
|
const events = []
|
|
@@ -75,6 +75,8 @@ async function runWeb({ session, userMessage, respond, locale = 'zh-CN', signal,
|
|
|
75
75
|
send: e => { events.push(e); onEvent?.(e) },
|
|
76
76
|
resume,
|
|
77
77
|
streamFn: streamFn ?? (opts => streamChatOnce({ ...opts, maxRetries: 0 })),
|
|
78
|
+
// 不传 = 让 runAgentTurn 自己读沙箱里的配置(要验默认取数时就别传)
|
|
79
|
+
...(promptSection === undefined ? {} : { promptSection }),
|
|
78
80
|
})
|
|
79
81
|
return { sent, events, result }
|
|
80
82
|
} finally {
|
|
@@ -264,6 +266,8 @@ test('400 且正文提到 tool/function 时提示换模型,而不是甩网关 JS
|
|
|
264
266
|
// · argsPreview —— 收起态那一行副标题,故意截到 200 字。
|
|
265
267
|
// 计划类工具的 steps 也在 arguments 里(不再单独开一条透传路径)。
|
|
266
268
|
test('tool_call_start 每种工具都带完整 arguments,argsPreview 仍是截断摘要', async () => {
|
|
269
|
+
// 桩的 write_file 落点:沙箱里(见 round 里的说明)。用完删掉,不留 tmp 垃圾。
|
|
270
|
+
const stubWritePath = path.join(SANDBOX, 'stub-write-target.js')
|
|
267
271
|
const planArgs = JSON.stringify({
|
|
268
272
|
steps: [
|
|
269
273
|
{ content: '读代码', status: 'completed' },
|
|
@@ -275,7 +279,12 @@ test('tool_call_start 每种工具都带完整 arguments,argsPreview 仍是截
|
|
|
275
279
|
sse([event({
|
|
276
280
|
choices: [{ delta: { tool_calls: [
|
|
277
281
|
{ index: 0, id: 'p1', function: { name: 'update_plan', arguments: planArgs } },
|
|
278
|
-
|
|
282
|
+
// ⚠️ 这个桩会走**真实** executeTool 落盘(这一层跑的是真 runAgentTurn,
|
|
283
|
+
// 不是解析 Mock)—— 路径必须指到沙箱。曾经这里写的是相对路径 'a.js':
|
|
284
|
+
// resolvePath 按 process.cwd() 解析,于是每次跑测试都在当前目录留一个
|
|
285
|
+
// 50 字节的 a.js(仓库根跑留根目录、routes/workbench 跑留那里),
|
|
286
|
+
// 全组用例还都是绿的,只能靠 git status 发现。
|
|
287
|
+
{ index: 1, id: 'p2', function: { name: 'write_file', arguments: JSON.stringify({ path: stubWritePath, content: 'x'.repeat(50) }) } }
|
|
279
288
|
] } }, { finish_reason: 'tool_calls' }],
|
|
280
289
|
}), 'data: [DONE]\n\n']),
|
|
281
290
|
okStream(),
|
|
@@ -283,29 +292,35 @@ test('tool_call_start 每种工具都带完整 arguments,argsPreview 仍是截
|
|
|
283
292
|
let turn = 0
|
|
284
293
|
const session = newSession('ag-sandbox-plan', withSystem())
|
|
285
294
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
295
|
+
try {
|
|
296
|
+
const { events } = await runWeb({
|
|
297
|
+
session,
|
|
298
|
+
userMessage: '把这个功能做出来',
|
|
299
|
+
respond: () => round[turn++] || okStream(),
|
|
300
|
+
})
|
|
291
301
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
302
|
+
const starts = events.filter(e => e.type === 'tool_call_start')
|
|
303
|
+
assert.equal(starts.length, 2)
|
|
304
|
+
const planStart = starts.find(e => e.name === 'update_plan')
|
|
305
|
+
const writeStart = starts.find(e => e.name === 'write_file')
|
|
306
|
+
|
|
307
|
+
// 计划:全文必须一字不差地带到前端(前端靠它渲染清单)
|
|
308
|
+
assert.equal(planStart.arguments, planArgs)
|
|
309
|
+
assert.match(planStart.argsPreview, /1\/2|1 完成/)
|
|
310
|
+
// 其他工具:展开态同样要看到原文,不再只给摘要
|
|
311
|
+
assert.equal(writeStart.arguments, JSON.stringify({ path: stubWritePath, content: 'x'.repeat(50) }))
|
|
312
|
+
assert.ok(writeStart.argsPreview.length < writeStart.arguments.length, '摘要仍应是截断过的')
|
|
313
|
+
// 顺带证明桩调用真的执行了、且只落在沙箱(这条红了 = 有人把路径改成会污染仓库的)
|
|
314
|
+
assert.ok(existsSync(stubWritePath), '桩的 write_file 应真实落在沙箱里')
|
|
315
|
+
|
|
316
|
+
const planResult = events.find(e => e.type === 'tool_result' && e.name === 'update_plan')
|
|
317
|
+
assert.match(planResult.result, /计划已更新/)
|
|
318
|
+
// 计划也要进会话记录 —— 重新打开会话时前端靠历史里的 arguments 复原清单
|
|
319
|
+
const assistantWithPlan = session.messages.find(m => m.role === 'assistant' && m.tool_calls?.some(t => t.function.name === 'update_plan'))
|
|
320
|
+
assert.equal(assistantWithPlan.tool_calls[0].function.arguments, planArgs)
|
|
321
|
+
} finally {
|
|
322
|
+
rmSync(stubWritePath, { force: true })
|
|
323
|
+
}
|
|
309
324
|
})
|
|
310
325
|
|
|
311
326
|
// ── 重试与续跑(2026-10-09)────────────────────────────────────
|
|
@@ -372,35 +387,111 @@ test('每次 LLM 请求前发一条 attempt(前端靠它记重试断点),
|
|
|
372
387
|
assert.ok(order.indexOf('attempt') < order.indexOf('content'), 'attempt 要在正文之前')
|
|
373
388
|
assert.ok(order.indexOf('content') < order.indexOf('retry'), 'retry 落在被丢弃的那段正文之后')
|
|
374
389
|
})
|
|
375
|
-
|
|
376
|
-
test('对跑完的一轮点「重新生成」:把尾巴那条 assistant 摘掉再问一次', async () => {
|
|
377
|
-
// 会话尾巴是一条没有工具调用的 assistant = 上一轮已经答完了。
|
|
378
|
-
// 留着它的话模型会顺着自己的答案往下说,重开会话还会看到两条连着的 assistant。
|
|
379
|
-
const session = newSession('ag-sandbox-regen', [
|
|
380
|
-
{ role: 'system', content: 'rules' },
|
|
381
|
-
{ role: 'user', content: 'hi' },
|
|
382
|
-
{ role: 'assistant', content: '旧答案' },
|
|
383
|
-
])
|
|
384
|
-
|
|
385
|
-
const { sent } = await runWeb({ session, userMessage: '', resume: true, respond: okStream })
|
|
386
|
-
|
|
387
|
-
assert.deepEqual(sent.messages.map(m => m.content), ['rules', 'hi'], '旧答案必须从请求里消失')
|
|
388
|
-
assert.equal(session.messages.filter(m => m.role === 'assistant').length, 1, '只剩这一次新生成的')
|
|
389
|
-
assert.equal(session.messages[session.messages.length - 1].content, 'ok')
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
test('对中途失败的一轮点「重试」:工具结果留在尾巴上,一条都不摘', async () => {
|
|
393
|
-
// 失败发生在工具循环中途时,尾巴是 assistant(tool_calls) + 一批 tool 结果 ——
|
|
394
|
-
// 这些是这一轮真干过的事,摘掉就等于让模型把工具再跑一遍。
|
|
395
|
-
const session = newSession('ag-sandbox-resume-tools', [
|
|
396
|
-
{ role: 'system', content: 'rules' },
|
|
397
|
-
{ role: 'user', content: 'hi' },
|
|
398
|
-
{ role: 'assistant', content: null, tool_calls: [call(0)] },
|
|
399
|
-
{ role: 'tool', tool_call_id: 'call0', content: 'file contents' },
|
|
400
|
-
])
|
|
401
|
-
|
|
402
|
-
const { sent } = await runWeb({ session, userMessage: '', resume: true, respond: okStream })
|
|
403
|
-
|
|
404
|
-
assert.deepEqual(sent.messages.map(m => m.role), ['system', 'user', 'assistant', 'tool'])
|
|
405
|
-
assert.equal(sent.messages[3].content, 'file contents', '已经跑完的工具结果必须原样带上')
|
|
406
|
-
})
|
|
390
|
+
|
|
391
|
+
test('对跑完的一轮点「重新生成」:把尾巴那条 assistant 摘掉再问一次', async () => {
|
|
392
|
+
// 会话尾巴是一条没有工具调用的 assistant = 上一轮已经答完了。
|
|
393
|
+
// 留着它的话模型会顺着自己的答案往下说,重开会话还会看到两条连着的 assistant。
|
|
394
|
+
const session = newSession('ag-sandbox-regen', [
|
|
395
|
+
{ role: 'system', content: 'rules' },
|
|
396
|
+
{ role: 'user', content: 'hi' },
|
|
397
|
+
{ role: 'assistant', content: '旧答案' },
|
|
398
|
+
])
|
|
399
|
+
|
|
400
|
+
const { sent } = await runWeb({ session, userMessage: '', resume: true, respond: okStream })
|
|
401
|
+
|
|
402
|
+
assert.deepEqual(sent.messages.map(m => m.content), ['rules', 'hi'], '旧答案必须从请求里消失')
|
|
403
|
+
assert.equal(session.messages.filter(m => m.role === 'assistant').length, 1, '只剩这一次新生成的')
|
|
404
|
+
assert.equal(session.messages[session.messages.length - 1].content, 'ok')
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('对中途失败的一轮点「重试」:工具结果留在尾巴上,一条都不摘', async () => {
|
|
408
|
+
// 失败发生在工具循环中途时,尾巴是 assistant(tool_calls) + 一批 tool 结果 ——
|
|
409
|
+
// 这些是这一轮真干过的事,摘掉就等于让模型把工具再跑一遍。
|
|
410
|
+
const session = newSession('ag-sandbox-resume-tools', [
|
|
411
|
+
{ role: 'system', content: 'rules' },
|
|
412
|
+
{ role: 'user', content: 'hi' },
|
|
413
|
+
{ role: 'assistant', content: null, tool_calls: [call(0)] },
|
|
414
|
+
{ role: 'tool', tool_call_id: 'call0', content: 'file contents' },
|
|
415
|
+
])
|
|
416
|
+
|
|
417
|
+
const { sent } = await runWeb({ session, userMessage: '', resume: true, respond: okStream })
|
|
418
|
+
|
|
419
|
+
assert.deepEqual(sent.messages.map(m => m.role), ['system', 'user', 'assistant', 'tool'])
|
|
420
|
+
assert.equal(sent.messages[3].content, 'file contents', '已经跑完的工具结果必须原样带上')
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
// ── 用户固定提示词(g ai 默认提示词 + 派发提示词)────────────────
|
|
424
|
+
// 智能体视图「设置」Tab 配的两块提示词,和 g ai 对话时每轮都要注入。
|
|
425
|
+
// 这是**纯 prompt 注入**的能力:没有按钮、没有状态、界面上看不出对错 ——
|
|
426
|
+
// 改 agentChat.js / chatPrompts.js 的人(哪怕只是把一段挪个位置)会无声地把它弄掉。
|
|
427
|
+
// 所以这里跑真实的 runAgentTurn,把发出去的请求体抓下来逐条钉住。
|
|
428
|
+
//
|
|
429
|
+
// 沙箱里写真实文件(config.json / orchestrator.json):这一层要验的正是
|
|
430
|
+
// "默认取数真的接到了配置上",用注入口反而绕过了要验的那一段。
|
|
431
|
+
// 每个用例 finally 里删干净 —— 同文件其它用例断言的是精确的 messages 列表,
|
|
432
|
+
// 残留一份提示词就会让它们莫名其妙地红。
|
|
433
|
+
|
|
434
|
+
const DATA_DIR = path.join(SANDBOX, '.zen-gitsync')
|
|
435
|
+
|
|
436
|
+
function writeSandboxFile(name, obj) {
|
|
437
|
+
mkdirSync(DATA_DIR, { recursive: true })
|
|
438
|
+
writeFileSync(path.join(DATA_DIR, name), JSON.stringify(obj, null, 2))
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function clearSandboxFile(name) {
|
|
442
|
+
rmSync(path.join(DATA_DIR, name), { force: true })
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
test('两块提示词都注入请求副本:g ai 默认提示词 + 派发提示词(含项目级)', async () => {
|
|
446
|
+
const { canonicalProjectPath } = await import('./projectRegistry.js')
|
|
447
|
+
const projectKey = canonicalProjectPath(process.cwd())
|
|
448
|
+
try {
|
|
449
|
+
writeSandboxFile('config.json', { agentDefaultPrompt: '【g ai 默认提示词】回答用中文' })
|
|
450
|
+
writeSandboxFile('orchestrator.json', {
|
|
451
|
+
version: 1,
|
|
452
|
+
active: true,
|
|
453
|
+
defaultPrompt: '【全局派发提示词】改完就提交',
|
|
454
|
+
projectPrompts: { [projectKey]: { path: process.cwd(), prompt: '【本项目派发提示词】包管理器用 pnpm' } },
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
const session = newSession('ag-sandbox-user-prompt', withSystem())
|
|
458
|
+
const { sent } = await runWeb({ session, userMessage: 'hello', respond: okStream })
|
|
459
|
+
|
|
460
|
+
const sys = sent.messages.find(m => m.role === 'system')
|
|
461
|
+
assert.match(sys.content, /【g ai 默认提示词】回答用中文/, 'g ai 默认提示词必须进请求')
|
|
462
|
+
assert.match(sys.content, /【全局派发提示词】改完就提交/, '全局派发提示词必须进请求')
|
|
463
|
+
assert.match(sys.content, /【本项目派发提示词】包管理器用 pnpm/,
|
|
464
|
+
'项目级那条要按本轮 cwd 生效(与派发时"按落点项目解析"同一口径)')
|
|
465
|
+
// 规则在前、现场在后:固定提示词是每轮都成立的约束,不该排在工作区快照后面
|
|
466
|
+
assert.ok(sys.content.indexOf('【g ai 默认提示词】') < sys.content.indexOf('【全局派发提示词】'),
|
|
467
|
+
'g ai 那段排在派发那段之前')
|
|
468
|
+
// 落盘口径:会话里那条 system 一个字节都不许变 —— 否则改设置后老会话永远带着旧规则
|
|
469
|
+
assert.equal(session.messages[0].content, 'rules', '注入只作用于请求副本,不写回 session.messages')
|
|
470
|
+
} finally {
|
|
471
|
+
clearSandboxFile('config.json')
|
|
472
|
+
clearSandboxFile('orchestrator.json')
|
|
473
|
+
}
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
test('没配就不注入(连一节标题都不出现)—— 这也是其它用例能断言精确 messages 的前提', async () => {
|
|
477
|
+
const session = newSession('ag-sandbox-user-prompt-empty', withSystem())
|
|
478
|
+
|
|
479
|
+
const { sent } = await runWeb({ session, userMessage: 'hello', respond: okStream })
|
|
480
|
+
|
|
481
|
+
assert.deepEqual(sent.messages.map(m => m.content), ['rules', 'hi', 'hello'])
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
test('调用方显式传 promptSection 时不再自己读配置(定时任务 / agentRoutes 走这条路)', async () => {
|
|
485
|
+
try {
|
|
486
|
+
// 磁盘上有一份提示词,但调用方传了 '':意思是"这一轮确定不带",不是"没传、你去读"
|
|
487
|
+
writeSandboxFile('config.json', { agentDefaultPrompt: '【不该出现】' })
|
|
488
|
+
const session = newSession('ag-sandbox-user-prompt-explicit', withSystem())
|
|
489
|
+
|
|
490
|
+
const { sent } = await runWeb({ session, userMessage: 'hello', respond: okStream, promptSection: '' })
|
|
491
|
+
|
|
492
|
+
assert.doesNotMatch(sent.messages[0].content, /【不该出现】/)
|
|
493
|
+
assert.deepEqual(sent.messages.map(m => m.content), ['rules', 'hi', 'hello'])
|
|
494
|
+
} finally {
|
|
495
|
+
clearSandboxFile('config.json')
|
|
496
|
+
}
|
|
497
|
+
})
|
|
@@ -32,6 +32,8 @@ import { registerAgentMarketplaceRoutes } from './agentMarketplace.js';
|
|
|
32
32
|
import { createProjectListProvider } from './projectTool.js';
|
|
33
33
|
import { createDispatcher } from './dispatchInstruction.js';
|
|
34
34
|
import { createWorkspaceSnapshotter } from '../aiContext/wiring.js';
|
|
35
|
+
// 用户固定提示词(g ai 默认提示词 + 派发提示词)的解析 —— 两条引擎链路共用同一份
|
|
36
|
+
import { resolveChatPromptSection } from './chatPrompts.js';
|
|
35
37
|
// 「同一会话同一时刻只允许一个生成中的轮次」的闸,与定时任务调度器共享
|
|
36
38
|
// (见 sessionGate.js —— 拆出去是为了让调度器复用同一份判定口径)。
|
|
37
39
|
import { isSessionBusy, acquireSession, releaseSession } from './sessionGate.js';
|
|
@@ -548,6 +550,18 @@ export function registerAgentRoutes({ app, getCurrentProjectPath, configManager,
|
|
|
548
550
|
}
|
|
549
551
|
}
|
|
550
552
|
|
|
553
|
+
// 用户固定提示词(g ai 默认提示词 + 派发提示词,见 chatPrompts.js)。
|
|
554
|
+
// 在这里解析**一次**、两条引擎链路共用:内置引擎走请求副本(runAgentTurn 的
|
|
555
|
+
// promptSection),外部 CLI 引擎没有副本可注入,只能拼进 prompt 前缀。
|
|
556
|
+
// 两处各解析一次的话,改设置后两条链路生效时机就会分叉。
|
|
557
|
+
// 取不到就当没配 —— 它是锦上添花,不该让用户这条消息发不出去。
|
|
558
|
+
let promptSection = '';
|
|
559
|
+
try {
|
|
560
|
+
promptSection = await resolveChatPromptSection({ cwd, locale });
|
|
561
|
+
} catch (err) {
|
|
562
|
+
logger.warn(`[agent/chat] 取用户固定提示词失败,本轮跳过注入: ${err?.message || err}`);
|
|
563
|
+
}
|
|
564
|
+
|
|
551
565
|
// 推 meta
|
|
552
566
|
send({
|
|
553
567
|
type: 'meta',
|
|
@@ -599,6 +613,11 @@ export function registerAgentRoutes({ app, getCurrentProjectPath, configManager,
|
|
|
599
613
|
if (dirStatusBlock) {
|
|
600
614
|
promptPrefix = promptPrefix ? `${dirStatusBlock}\n\n${promptPrefix}` : dirStatusBlock;
|
|
601
615
|
}
|
|
616
|
+
// 用户固定提示词排在最前:它是每轮都成立的规则,工作区快照是这一刻的现场。
|
|
617
|
+
// 外部引擎那条链路的 system 提示词是 CLI 自己的,进不去 —— 只能靠这个前缀。
|
|
618
|
+
if (promptSection) {
|
|
619
|
+
promptPrefix = promptPrefix ? `${promptSection}\n\n${promptPrefix}` : promptSection;
|
|
620
|
+
}
|
|
602
621
|
// 图片附件转成文件路径交给 CLI(它们收路径不收 base64)。走同一套落盘校验。
|
|
603
622
|
const imageAttachments = images.map((u, i) => ({ name: `pasted-${i + 1}.png`, dataUrl: u }));
|
|
604
623
|
const imageFiles = imageAttachments.length > 0
|
|
@@ -654,7 +673,10 @@ export function registerAgentRoutes({ app, getCurrentProjectPath, configManager,
|
|
|
654
673
|
// 与 CLI turn.js 同一条口径。
|
|
655
674
|
requestBudget,
|
|
656
675
|
// 重试上一轮:不再压 user 消息,接着会话现有的尾巴跑
|
|
657
|
-
resume
|
|
676
|
+
resume,
|
|
677
|
+
// 用户固定提示词(上面已解析)。显式传 —— 不传的话 runAgentTurn 会自己再读一遍
|
|
678
|
+
// 配置文件,同一轮里读两次同一份配置纯属浪费
|
|
679
|
+
promptSection
|
|
658
680
|
});
|
|
659
681
|
}
|
|
660
682
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
// 「和 g ai 对话时固定注入的两块提示词」—— 拼装规则(纯函数)+ 取数(读两处配置)。
|
|
16
|
+
//
|
|
17
|
+
// 两块分别来自两处,语义也不同,别混:
|
|
18
|
+
// ① g ai 智能体的默认提示词 —— config.json 的 agentDefaultPrompt,
|
|
19
|
+
// 在「智能体视图 → 设置」里编辑。只喂给 g ai 自己。
|
|
20
|
+
// ② 派发任务的提示词 —— orchestrator.json 的 defaultPrompt(+ 落点项目的 projectPrompts),
|
|
21
|
+
// 和工作台「预设提示词」是**同一份**(同一套读写接口,同一处解析)。
|
|
22
|
+
// 它的原生用途是"派发任务时附加在指令之前"(见 orchestratorStore.resolveDispatchPrompt);
|
|
23
|
+
// 这里把同一份也喂给对话中的 g ai —— 用户在 g ai 里说的"帮我改完就提交"
|
|
24
|
+
// 本来就依赖这些约束,让它在对话里也看得见。
|
|
25
|
+
//
|
|
26
|
+
// ⚠️ 为什么注入的是**每轮请求副本**而不是会话里的那条 system 消息:
|
|
27
|
+
// 会话的 system 消息只在首轮写入并落盘,之后改设置要新开会话才生效,而且会把
|
|
28
|
+
// 当时的配置永久腌进会话文件(回看历史会话时看到的是过期规则,还占上下文)。
|
|
29
|
+
// 注入请求副本则每轮现读、立即生效、不落盘 —— 与工作区快照/附件路径同一套口径
|
|
30
|
+
// (见 agentChat.js 的 injectRequestContext)。
|
|
31
|
+
//
|
|
32
|
+
// ⚠️ 解析失败一律当作"没有配置"(返回空串),绝不抛:配置坏了不该让用户发不出消息。
|
|
33
|
+
// 与 workspaceBlock 的兜底口径一致。
|
|
34
|
+
|
|
35
|
+
import configManager from '../../../../config.js';
|
|
36
|
+
import { logger } from './shared.js';
|
|
37
|
+
import { readOrchestrator, resolveDispatchPrompt } from './orchestratorStore.js';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 拼「用户固定提示词」这一节。
|
|
41
|
+
*
|
|
42
|
+
* 纯函数:只吃入参、不做 IO,单测不碰真实用户数据(与 promptParts.js / memoryContext.js
|
|
43
|
+
* 同一个理由)。取数在 resolveChatPromptSection。
|
|
44
|
+
*
|
|
45
|
+
* @param {object} input
|
|
46
|
+
* @param {string} [input.gaiPrompt] g ai 默认提示词('' = 没设)
|
|
47
|
+
* @param {string} [input.dispatchPrompt] 派发任务提示词(全局 + 落点项目,'' = 没设)
|
|
48
|
+
* @param {string} [input.locale] 'zh' | 'en' —— 与系统提示词同一套判定
|
|
49
|
+
* @returns {string} 两块都空时返回 ''(调用方据此完全跳过注入)
|
|
50
|
+
*/
|
|
51
|
+
export function buildChatPromptSection({ gaiPrompt = '', dispatchPrompt = '', locale = 'zh-CN' } = {}) {
|
|
52
|
+
const gai = typeof gaiPrompt === 'string' ? gaiPrompt.trim() : '';
|
|
53
|
+
const dispatch = typeof dispatchPrompt === 'string' ? dispatchPrompt.trim() : '';
|
|
54
|
+
if (!gai && !dispatch) return '';
|
|
55
|
+
|
|
56
|
+
const en = String(locale || '').startsWith('en');
|
|
57
|
+
const parts = [];
|
|
58
|
+
if (gai) parts.push(`${en ? '### g ai default prompt' : '### g ai 默认提示词'}\n${gai}`);
|
|
59
|
+
if (dispatch) {
|
|
60
|
+
// 括号里那句是给模型看的(也是给排查的人看的):这一块的来源与工作台是同一份,
|
|
61
|
+
// 不是对话专属的又一份配置 —— 模型据此知道"这些约束在派发出去的任务里同样成立"。
|
|
62
|
+
parts.push(en
|
|
63
|
+
? `### Dispatch prompt (same text attached to tasks dispatched from the workbench)\n${dispatch}`
|
|
64
|
+
: `### 派发任务的提示词(与工作台派发任务时附加的是同一份)\n${dispatch}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const head = en
|
|
68
|
+
? 'The user configured the following standing instructions in Settings. They apply to every turn of this conversation — follow them alongside your built-in rules.'
|
|
69
|
+
: '以下是用户在「设置」里配置的固定提示词,对本轮对话生效 —— 请与你的内置规则一并遵守。';
|
|
70
|
+
|
|
71
|
+
return `${head}\n\n${parts.join('\n\n')}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 默认取数:config.json 的 agentDefaultPrompt(读不到 = 空) */
|
|
75
|
+
async function defaultLoadAgentPrompt() {
|
|
76
|
+
const cfg = await configManager.loadConfig();
|
|
77
|
+
return typeof cfg?.agentDefaultPrompt === 'string' ? cfg.agentDefaultPrompt : '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 默认取数:orchestrator.json 里按**落点项目**解析出的派发提示词(读不到 = 空) */
|
|
81
|
+
async function defaultLoadDispatchPrompt(cwd) {
|
|
82
|
+
const state = await readOrchestrator();
|
|
83
|
+
return resolveDispatchPrompt(state, cwd).text;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 解析一次对话实际要注入的两块提示词。
|
|
88
|
+
*
|
|
89
|
+
* @param {object} input
|
|
90
|
+
* @param {string} [input.cwd] 本轮对话的工作目录(决定项目级那条派发提示词生不生效)
|
|
91
|
+
* @param {string} [input.locale]
|
|
92
|
+
* @param {Function} [input.loadAgentPrompt] 注入口(单测用;生产走 config.json)
|
|
93
|
+
* @param {Function} [input.loadDispatchPrompt] 注入口(同上;生产走 orchestrator.json)
|
|
94
|
+
* @returns {Promise<string>} 拼好的正文,'' = 不注入
|
|
95
|
+
*/
|
|
96
|
+
export async function resolveChatPromptSection({
|
|
97
|
+
cwd = '',
|
|
98
|
+
locale = 'zh-CN',
|
|
99
|
+
loadAgentPrompt = defaultLoadAgentPrompt,
|
|
100
|
+
loadDispatchPrompt = defaultLoadDispatchPrompt,
|
|
101
|
+
} = {}) {
|
|
102
|
+
let gaiPrompt = '';
|
|
103
|
+
let dispatchPrompt = '';
|
|
104
|
+
try {
|
|
105
|
+
gaiPrompt = await loadAgentPrompt();
|
|
106
|
+
} catch (err) {
|
|
107
|
+
logger.warn(`[chatPrompts] 读 g ai 默认提示词失败,本轮跳过: ${err?.message || err}`);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
dispatchPrompt = await loadDispatchPrompt(cwd);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
logger.warn(`[chatPrompts] 读派发提示词失败,本轮跳过: ${err?.message || err}`);
|
|
113
|
+
}
|
|
114
|
+
return buildChatPromptSection({ gaiPrompt, dispatchPrompt, locale });
|
|
115
|
+
}
|