@huicai/cli 2026.711.0 → 2026.711.24617
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/LICENSE +43 -21
- package/package.json +4 -3
- package/src/advisor.js +1 -46
- package/src/agent.js +1 -518
- package/src/api.js +1 -359
- package/src/autofix.js +1 -90
- package/src/banner.js +1 -83
- package/src/checkpoints.js +1 -99
- package/src/clipboard.js +1 -34
- package/src/codeindex.js +1 -229
- package/src/colors.js +1 -47
- package/src/commands.js +1 -1294
- package/src/compaction.js +1 -144
- package/src/config.js +1 -86
- package/src/consult.js +1 -65
- package/src/context-report.js +1 -119
- package/src/context.js +1 -152
- package/src/cron.js +1 -64
- package/src/daemon.js +1 -62
- package/src/device.js +1 -88
- package/src/diffview.js +1 -115
- package/src/doctor.js +1 -72
- package/src/executors.js +1 -391
- package/src/forks.js +1 -59
- package/src/gitflow.js +1 -140
- package/src/hooks.js +1 -207
- package/src/ide.js +1 -53
- package/src/input.js +1 -182
- package/src/insights.js +1 -102
- package/src/kbsync.js +1 -141
- package/src/markdown.js +0 -0
- package/src/markup-filter.js +1 -85
- package/src/mcp.js +1 -207
- package/src/memory.js +1 -161
- package/src/permissions.js +1 -120
- package/src/render.js +1 -138
- package/src/repl.js +1 -271
- package/src/scan.js +1 -84
- package/src/schedule.js +1 -48
- package/src/session.js +1 -140
- package/src/settings.js +0 -0
- package/src/skills.js +1 -248
- package/src/subagent.js +1 -183
- package/src/todo.js +1 -75
- package/src/tools.js +1 -147
- package/src/transcript.js +1 -94
- package/src/tui/app.js +1 -666
- package/src/usage.js +1 -43
- package/src/watch.js +1 -74
package/src/compaction.js
CHANGED
|
@@ -1,144 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 仅在「发给模型」时用 buildModelMessages() 组装视图:system + 摘要占位 + 未压缩的近期消息。
|
|
3
|
-
// 因此 UI 已显示的对话、落盘会话、分享页/扩展/CLI 的正文都保持完整,压缩只影响送入模型的上下文。
|
|
4
|
-
//
|
|
5
|
-
// state 形状:{ upTo: number, summary: string }
|
|
6
|
-
// messages[1..upTo) 已被摘要覆盖(0 号是 system);messages[upTo..] 原样发送。
|
|
7
|
-
// 触发条件(满足其一):有效上下文字符超阈值;上一轮真实 prompt_tokens 超限。compactNow 手动强制。
|
|
8
|
-
// 切割边界永远取 user 消息下标,天然不拆 assistant(tool_calls) 与其 tool 结果的配对。
|
|
9
|
-
|
|
10
|
-
import { streamChat } from './api.js';
|
|
11
|
-
import { DEFAULT_MODEL } from './config.js';
|
|
12
|
-
import { stripMarkup } from './markup-filter.js';
|
|
13
|
-
|
|
14
|
-
const DEFAULT_THRESHOLD_CHARS = 48000; // ≈ 16k tokens
|
|
15
|
-
const DEFAULT_AUTOCOMPACT_TOKENS = 80000; // 上一轮 prompt_tokens 超此值触发(多数模型 128k 窗口的 ~60%)
|
|
16
|
-
const KEEP_RECENT_USER_TURNS = 3;
|
|
17
|
-
|
|
18
|
-
function totalChars(messages) {
|
|
19
|
-
let n = 0;
|
|
20
|
-
for (const m of messages) {
|
|
21
|
-
if (m.content) n += String(m.content).length;
|
|
22
|
-
if (m.tool_calls) n += JSON.stringify(m.tool_calls).length;
|
|
23
|
-
}
|
|
24
|
-
return n;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** 组装实际发给模型的消息视图(原 messages 不动)。state 为空/无摘要时原样返回。
|
|
28
|
-
* 首条消息(通常是 system)永不被摘要覆盖,始终原样保留。 */
|
|
29
|
-
export function buildModelMessages(messages, state) {
|
|
30
|
-
if (!state || !state.summary || !(state.upTo > 1) || state.upTo > messages.length) return messages;
|
|
31
|
-
return [
|
|
32
|
-
...messages.slice(0, 1),
|
|
33
|
-
{
|
|
34
|
-
role: 'user',
|
|
35
|
-
content: '【上下文压缩】以下是此前多轮对话与工具执行的摘要(原始对话仍完整保留在会话中,仅为节省上下文而摘要):\n'
|
|
36
|
-
+ state.summary,
|
|
37
|
-
},
|
|
38
|
-
...messages.slice(state.upTo),
|
|
39
|
-
];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 若超阈值则把较早回合摘要进 state(原地更新 state,不动 messages)。
|
|
44
|
-
* @param {any[]} messages 完整消息(含已被摘要覆盖的部分,永不被修改)
|
|
45
|
-
* @param {object} config baseUrl/apiKey/model/autoCompactTokens
|
|
46
|
-
* @param {object} [opts] {state, thresholdChars, keepRecent, force, lastPromptTokens}
|
|
47
|
-
* @returns {Promise<number>} 本次新增被摘要覆盖的消息条数(0=未压缩)
|
|
48
|
-
*/
|
|
49
|
-
export async function compactIfNeeded(messages, config, opts = {}) {
|
|
50
|
-
const state = opts.state || {};
|
|
51
|
-
const keep = opts.keepRecent || KEEP_RECENT_USER_TURNS;
|
|
52
|
-
if (!messages || messages.length < 6) return 0;
|
|
53
|
-
|
|
54
|
-
// /autocompact 配置:负数=关闭自动压缩(仍可手动 force);正数=自定义 token 阈值(并按 ~3 字符/token 联动字符阈值)
|
|
55
|
-
const configuredTokens = Number(config?.autoCompactTokens);
|
|
56
|
-
if (!opts.force && Number.isFinite(configuredTokens) && configuredTokens < 0) return 0;
|
|
57
|
-
const tokenLimit = configuredTokens > 0 ? configuredTokens : DEFAULT_AUTOCOMPACT_TOKENS;
|
|
58
|
-
const threshold = opts.thresholdChars || (configuredTokens > 0 ? configuredTokens * 3 : DEFAULT_THRESHOLD_CHARS);
|
|
59
|
-
|
|
60
|
-
const effective = buildModelMessages(messages, state);
|
|
61
|
-
if (!opts.force) {
|
|
62
|
-
const overTokens = Number(opts.lastPromptTokens) > tokenLimit;
|
|
63
|
-
const overChars = totalChars(effective) > threshold;
|
|
64
|
-
if (!overTokens && !overChars) return 0;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 候选切点:位于当前 upTo 之后的 user 消息下标;保留最近 keep 个用户轮
|
|
68
|
-
const from = Math.max(Number(state.upTo) || 1, 1);
|
|
69
|
-
const userIdx = [];
|
|
70
|
-
for (let i = from; i < messages.length; i++) if (messages[i].role === 'user') userIdx.push(i);
|
|
71
|
-
if (userIdx.length <= keep) return 0; // 新增部分轮数太少,无可压
|
|
72
|
-
const newUpTo = userIdx[userIdx.length - keep];
|
|
73
|
-
if (newUpTo - from < 2) return 0;
|
|
74
|
-
|
|
75
|
-
const slice = messages.slice(from, newUpTo);
|
|
76
|
-
let summary;
|
|
77
|
-
try {
|
|
78
|
-
summary = await summarize(slice, config, state.summary);
|
|
79
|
-
} catch {
|
|
80
|
-
return 0; // 摘要失败:不压缩(宁可上下文长,也不丢历史)
|
|
81
|
-
}
|
|
82
|
-
if (!summary || !summary.trim()) return 0;
|
|
83
|
-
|
|
84
|
-
state.summary = summary.trim();
|
|
85
|
-
state.upTo = newUpTo;
|
|
86
|
-
return newUpTo - from;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** 手动强制压缩(/compact):忽略阈值。返回本次被摘要覆盖的消息条数。 */
|
|
90
|
-
export function compactNow(messages, config, opts = {}) {
|
|
91
|
-
return compactIfNeeded(messages, config, { ...opts, force: true });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 伪模型(agent-/workflow-/datahuman-)在服务端走完整编排(RAG/工具/计费)。
|
|
95
|
-
// 摘要是纯内部辅助调用,必须与用户的业务对话彻底分离:绝不能用伪模型跑摘要,
|
|
96
|
-
// 否则每次压缩都会额外触发一次智能体执行(副作用+计费),且人格会污染摘要内容。
|
|
97
|
-
const PSEUDO_MODEL_RE = /^(agent|workflow|datahuman)-/;
|
|
98
|
-
|
|
99
|
-
/** 摘要专用模型:显式 compactModel 优先;当前模型为伪模型时回退内置默认原始模型。 */
|
|
100
|
-
function summaryModel(config) {
|
|
101
|
-
if (config?.compactModel) return config.compactModel;
|
|
102
|
-
const m = config?.model || '';
|
|
103
|
-
return PSEUDO_MODEL_RE.test(m) ? DEFAULT_MODEL : m;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function summarize(slice, config, prevSummary) {
|
|
107
|
-
const parts = [];
|
|
108
|
-
if (prevSummary && prevSummary.trim()) parts.push('【先前的摘要(需并入新摘要)】\n' + prevSummary.trim());
|
|
109
|
-
parts.push(renderForSummary(slice));
|
|
110
|
-
const r = await streamChat({
|
|
111
|
-
baseUrl: config.baseUrl,
|
|
112
|
-
apiKey: config.apiKey,
|
|
113
|
-
model: summaryModel(config),
|
|
114
|
-
messages: [
|
|
115
|
-
{
|
|
116
|
-
role: 'system',
|
|
117
|
-
content:
|
|
118
|
-
'你是对话/执行记录压缩器。把下面的记录(含可能存在的先前摘要)压缩成一份简洁但信息完整的中文摘要:'
|
|
119
|
-
+ '保留已完成的关键步骤、工具得到的重要事实/数据/结论、涉及的文件路径、尚未解决的问题与下一步线索。'
|
|
120
|
-
+ '只依据记录、不要臆造、不要寒暄。',
|
|
121
|
-
},
|
|
122
|
-
{ role: 'user', content: parts.join('\n\n') },
|
|
123
|
-
],
|
|
124
|
-
}); // 无 handlers:非流式收集
|
|
125
|
-
// 摘要会注入后续上下文:清洗文本化工具标记,防止协议乱码经摘要回流、继续诱发模型格式退化
|
|
126
|
-
return stripMarkup(r.content).text;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function renderForSummary(messages) {
|
|
130
|
-
const lines = [];
|
|
131
|
-
for (const m of messages) {
|
|
132
|
-
if (m.role === 'assistant' && m.tool_calls?.length) {
|
|
133
|
-
const calls = m.tool_calls
|
|
134
|
-
.map((tc) => `${tc.function?.name}(${String(tc.function?.arguments || '').slice(0, 200)})`)
|
|
135
|
-
.join(' ');
|
|
136
|
-
lines.push(`[助手调用工具] ${calls}${m.content ? ' | ' + m.content : ''}`);
|
|
137
|
-
} else if (m.role === 'tool') {
|
|
138
|
-
lines.push(`[工具结果] ${String(m.content || '').slice(0, 800)}`);
|
|
139
|
-
} else if (m.content) {
|
|
140
|
-
lines.push(`[${m.role}] ${m.content}`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return lines.join('\n');
|
|
144
|
-
}
|
|
1
|
+
import{streamChat as t}from"./api.js";import{DEFAULT_MODEL as e}from"./config.js";import{stripMarkup as n}from"./markup-filter.js";export function buildModelMessages(t,e){return e&&e.summary&&e.upTo>1&&!(e.upTo>t.length)?[...t.slice(0,1),{role:"user",content:"【上下文压缩】以下是此前多轮对话与工具执行的摘要(原始对话仍完整保留在会话中,仅为节省上下文而摘要):\n"+e.summary},...t.slice(e.upTo)]:t}export async function compactIfNeeded(e,o,s={}){const c=s.state||{},l=s.keepRecent||3;if(!e||e.length<6)return 0;const u=Number(o?.autoCompactTokens);if(!s.force&&Number.isFinite(u)&&u<0)return 0;const i=u>0?u:8e4,a=s.thresholdChars||(u>0?3*u:48e3),m=buildModelMessages(e,c);if(!s.force){const t=Number(s.lastPromptTokens)>i,e=function(t){let e=0;for(const n of t)n.content&&(e+=String(n.content).length),n.tool_calls&&(e+=JSON.stringify(n.tool_calls).length);return e}(m)>a;if(!t&&!e)return 0}const f=Math.max(Number(c.upTo)||1,1),p=[];for(let t=f;t<e.length;t++)"user"===e[t].role&&p.push(t);if(p.length<=l)return 0;const h=p[p.length-l];if(h-f<2)return 0;const g=e.slice(f,h);let d;try{d=await async function(e,o,s){const c=[];s&&s.trim()&&c.push("【先前的摘要(需并入新摘要)】\n"+s.trim());c.push(function(t){const e=[];for(const n of t)if("assistant"===n.role&&n.tool_calls?.length){const t=n.tool_calls.map(t=>`${t.function?.name}(${String(t.function?.arguments||"").slice(0,200)})`).join(" ");e.push(`[助手调用工具] ${t}${n.content?" | "+n.content:""}`)}else"tool"===n.role?e.push(`[工具结果] ${String(n.content||"").slice(0,800)}`):n.content&&e.push(`[${n.role}] ${n.content}`);return e.join("\n")}(e));const l=await t({baseUrl:o.baseUrl,apiKey:o.apiKey,model:r(o),messages:[{role:"system",content:"你是对话/执行记录压缩器。把下面的记录(含可能存在的先前摘要)压缩成一份简洁但信息完整的中文摘要:保留已完成的关键步骤、工具得到的重要事实/数据/结论、涉及的文件路径、尚未解决的问题与下一步线索。只依据记录、不要臆造、不要寒暄。"},{role:"user",content:c.join("\n\n")}]});return n(l.content).text}(g,o,c.summary)}catch{return 0}return d&&d.trim()?(c.summary=d.trim(),c.upTo=h,h-f):0}export function compactNow(t,e,n={}){return compactIfNeeded(t,e,{...n,force:!0})}const o=/^(agent|workflow|datahuman)-/;function r(t){if(t?.compactModel)return t.compactModel;const n=t?.model||"";return o.test(n)?e:n}
|
package/src/config.js
CHANGED
|
@@ -1,86 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// apiKey 敏感:写入时置文件权限 0600。
|
|
3
|
-
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import os from 'node:os';
|
|
7
|
-
|
|
8
|
-
export const CONFIG_DIR = path.join(os.homedir(), '.huicai');
|
|
9
|
-
export const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
10
|
-
|
|
11
|
-
/** 平台默认地址(内置,用户无需输入网关地址;可用 HUICAI_BASE_URL / --base 覆盖,供自建/联调)。 */
|
|
12
|
-
export const DEFAULT_BASE_URL = 'https://huicai-ai.sjht.com';
|
|
13
|
-
|
|
14
|
-
/** 内置默认模型(用户未设置时用它;登录时若账号有它则优先,否则回退到账号的第一个原始模型)。 */
|
|
15
|
-
export const DEFAULT_MODEL = 'deepseek-v4-flash';
|
|
16
|
-
|
|
17
|
-
function readFileConfig() {
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
20
|
-
} catch {
|
|
21
|
-
return {};
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 解析生效配置:环境变量 > 配置文件 > 缺省。
|
|
27
|
-
* @returns {{baseUrl:string, apiKey:string, model:string}}
|
|
28
|
-
*/
|
|
29
|
-
export function loadConfig() {
|
|
30
|
-
const file = readFileConfig();
|
|
31
|
-
const cfg = {
|
|
32
|
-
// 网关地址内置默认,用户无需输入;仍可用环境变量/配置文件覆盖(自建或联调)
|
|
33
|
-
baseUrl: (process.env.HUICAI_BASE_URL || file.baseUrl || DEFAULT_BASE_URL).trim(),
|
|
34
|
-
apiKey: (process.env.HUICAI_API_KEY || file.apiKey || '').trim(),
|
|
35
|
-
model: (process.env.HUICAI_MODEL || file.model || DEFAULT_MODEL).trim(),
|
|
36
|
-
// 单回合最大工具轮数(0/缺省 = 用内置默认 60);可被 --max-turns 覆盖
|
|
37
|
-
maxTurns: Number(process.env.HUICAI_MAX_TURNS || file.maxTurns) || 0,
|
|
38
|
-
// 自动压缩触发的 prompt_tokens 上限(0=用 compaction 内置默认 80000;负=/autocompact off 关闭)
|
|
39
|
-
autoCompactTokens: Number(file.autoCompactTokens) || 0,
|
|
40
|
-
// 推理强度(reasoning_effort):minimal|low|medium|high;空串=不透传(由模型默认)。模型不支持则被忽略。
|
|
41
|
-
reasoningEffort: (process.env.HUICAI_EFFORT || file.reasoningEffort || '').trim(),
|
|
42
|
-
// 压缩摘要专用模型(缺省:当前模型;当前为伪模型时 compaction 内部回退 DEFAULT_MODEL——摘要绝不经智能体编排)
|
|
43
|
-
compactModel: (process.env.HUICAI_COMPACT_MODEL || file.compactModel || '').trim(),
|
|
44
|
-
// 长期记忆(memory_write + 系统提示词注入):默认开;配置 memory:false 或 HUICAI_MEMORY=0 关闭
|
|
45
|
-
memoryEnabled: process.env.HUICAI_MEMORY === '0' ? false : file.memory !== false,
|
|
46
|
-
// 思考过程流式展示:默认开;配置 showReasoning:false 或 HUICAI_REASONING=0 关闭
|
|
47
|
-
showReasoning: process.env.HUICAI_REASONING === '0' ? false : file.showReasoning !== false,
|
|
48
|
-
// 助手回复 Markdown 渲染(TUI 富文本):默认开;配置 markdown:false 或 HUICAI_MARKDOWN=0 关闭(显示原始文本)
|
|
49
|
-
markdown: process.env.HUICAI_MARKDOWN === '0' ? false : file.markdown !== false,
|
|
50
|
-
// 客户端来源标识(对话落库审计用):默认 cli;VSCode 扩展进程内置 HUICAI_CLIENT=vscode
|
|
51
|
-
client: (process.env.HUICAI_CLIENT || 'cli').trim().toLowerCase(),
|
|
52
|
-
// 输出主题:default(彩色)| mono(无色)。终端自身掌控具体配色,这里主要控制开关。
|
|
53
|
-
theme: (process.env.HUICAI_THEME || file.theme || 'default').trim(),
|
|
54
|
-
// 强调色(提示符/高亮):cyan|green|magenta|blue|yellow|red
|
|
55
|
-
accentColor: (file.accentColor || 'cyan').trim(),
|
|
56
|
-
// 交互渲染器偏好(下次启动生效):auto(TTY 下富 TUI)| rich | plain
|
|
57
|
-
tui: (process.env.HUICAI_TUI || file.tui || 'auto').trim(),
|
|
58
|
-
// 调试输出:每轮打印模型/耗时/token 等(默认关)
|
|
59
|
-
debug: process.env.HUICAI_DEBUG === '1' ? true : file.debug === true,
|
|
60
|
-
// 自定义状态栏命令(TUI 底栏展示其 stdout;空=用默认)
|
|
61
|
-
statusLine: (file.statusLine || '').trim(),
|
|
62
|
-
// 顾问模型(更强的模型):设置后暴露 ask_advisor 工具,供智能体遇难题时咨询第二意见
|
|
63
|
-
advisorModel: (process.env.HUICAI_ADVISOR || file.advisorModel || '').trim(),
|
|
64
|
-
// 按任务自动选模型:设一个更强模型 → 复杂回合(长提示/难点关键词/计划模式)自动用它,其余用当前(便宜)模型
|
|
65
|
-
autoModelStrong: (process.env.HUICAI_AUTOMODEL || file.autoModelStrong || '').trim(),
|
|
66
|
-
// 本项目 token 预算(软提醒;0=不限)
|
|
67
|
-
budgetTokens: Number(file.budgetTokens) || 0,
|
|
68
|
-
// 语义检索用嵌入模型:设置后 search_code 在 BM25 召回上做向量重排(真语义);空=纯 BM25
|
|
69
|
-
embeddingModel: (process.env.HUICAI_EMBED_MODEL || file.embeddingModel || '').trim(),
|
|
70
|
-
};
|
|
71
|
-
cfg.baseUrl = cfg.baseUrl.replace(/\/+$/, ''); // 去尾斜杠
|
|
72
|
-
return cfg;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** 合并写入配置文件(0600),返回合并结果。 */
|
|
76
|
-
export function saveConfig(partial) {
|
|
77
|
-
const merged = { ...readFileConfig(), ...partial };
|
|
78
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
79
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
|
|
80
|
-
try {
|
|
81
|
-
fs.chmodSync(CONFIG_PATH, 0o600);
|
|
82
|
-
} catch {
|
|
83
|
-
// 非 POSIX 忽略
|
|
84
|
-
}
|
|
85
|
-
return merged;
|
|
86
|
-
}
|
|
1
|
+
import e from"node:fs";import o from"node:path";import r from"node:os";export const CONFIG_DIR=o.join(r.homedir(),".huicai");export const CONFIG_PATH=o.join(CONFIG_DIR,"config.json");export const DEFAULT_BASE_URL="https://huicai-ai.sjht.com";export const DEFAULT_MODEL="deepseek-v4-flash";function n(){try{return JSON.parse(e.readFileSync(CONFIG_PATH,"utf8"))}catch{return{}}}export function loadConfig(){const e=n(),o={baseUrl:(process.env.HUICAI_BASE_URL||e.baseUrl||DEFAULT_BASE_URL).trim(),apiKey:(process.env.HUICAI_API_KEY||e.apiKey||"").trim(),model:(process.env.HUICAI_MODEL||e.model||DEFAULT_MODEL).trim(),maxTurns:Number(process.env.HUICAI_MAX_TURNS||e.maxTurns)||0,autoCompactTokens:Number(e.autoCompactTokens)||0,reasoningEffort:(process.env.HUICAI_EFFORT||e.reasoningEffort||"").trim(),compactModel:(process.env.HUICAI_COMPACT_MODEL||e.compactModel||"").trim(),memoryEnabled:"0"!==process.env.HUICAI_MEMORY&&!1!==e.memory,showReasoning:"0"!==process.env.HUICAI_REASONING&&!1!==e.showReasoning,markdown:"0"!==process.env.HUICAI_MARKDOWN&&!1!==e.markdown,client:(process.env.HUICAI_CLIENT||"cli").trim().toLowerCase(),theme:(process.env.HUICAI_THEME||e.theme||"default").trim(),accentColor:(e.accentColor||"cyan").trim(),tui:(process.env.HUICAI_TUI||e.tui||"auto").trim(),debug:"1"===process.env.HUICAI_DEBUG||!0===e.debug,statusLine:(e.statusLine||"").trim(),advisorModel:(process.env.HUICAI_ADVISOR||e.advisorModel||"").trim(),autoModelStrong:(process.env.HUICAI_AUTOMODEL||e.autoModelStrong||"").trim(),budgetTokens:Number(e.budgetTokens)||0,embeddingModel:(process.env.HUICAI_EMBED_MODEL||e.embeddingModel||"").trim()};return o.baseUrl=o.baseUrl.replace(/\/+$/,""),o}export function saveConfig(o){const r={...n(),...o};e.mkdirSync(CONFIG_DIR,{recursive:!0}),e.writeFileSync(CONFIG_PATH,JSON.stringify(r,null,2)+"\n",{mode:384});try{e.chmodSync(CONFIG_PATH,384)}catch{}return r}
|
package/src/consult.js
CHANGED
|
@@ -1,65 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 暴露给本机智能体循环。本机用原始驱动模型 + 本地工具(读写文件/执行命令/子智能体…),遇到平台目标擅长的问题
|
|
3
|
-
// 调用 consult_platform——由平台侧编排作答(服务端知识库 RAG / 工具 / 联网),再用本机工具把结论落地到项目。
|
|
4
|
-
|
|
5
|
-
import { streamChat } from './api.js';
|
|
6
|
-
|
|
7
|
-
const PSEUDO = /^(agent|workflow|datahuman)-/;
|
|
8
|
-
|
|
9
|
-
/** 平台伪模型 id → 中文类别名。 */
|
|
10
|
-
function kindOf(ref) {
|
|
11
|
-
if (ref.startsWith('workflow-')) return '工作流';
|
|
12
|
-
if (ref.startsWith('datahuman-')) return '业务数字人';
|
|
13
|
-
return '智能体';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 构造 consult_platform 工具;未配置或 ref 非平台伪模型时返回 null。
|
|
18
|
-
* @param {object} p
|
|
19
|
-
* @param {object} p.config 运行配置(读时取最新;含 baseUrl/apiKey/consult/consultLabel/client/sessionId)
|
|
20
|
-
*/
|
|
21
|
-
export function makeConsultTool({ config }) {
|
|
22
|
-
const ref = config && config.consult;
|
|
23
|
-
if (!ref || !PSEUDO.test(String(ref))) return null;
|
|
24
|
-
const kind = kindOf(String(ref));
|
|
25
|
-
const label = config.consultLabel || ref;
|
|
26
|
-
|
|
27
|
-
const spec = {
|
|
28
|
-
type: 'function',
|
|
29
|
-
function: {
|
|
30
|
-
name: 'consult_platform',
|
|
31
|
-
description:
|
|
32
|
-
`咨询平台${kind}「${label}」:由平台侧编排作答(带其服务端知识库 RAG / 工具 / 联网检索)。`
|
|
33
|
-
+ `遇到它擅长领域的问题优先调用它获取权威答复,再用本机工具把结论落地到项目。可多次调用。`,
|
|
34
|
-
parameters: {
|
|
35
|
-
type: 'object',
|
|
36
|
-
properties: {
|
|
37
|
-
query: { type: 'string', description: '自然语言的咨询 / 查询 / 运行输入' },
|
|
38
|
-
},
|
|
39
|
-
required: ['query'],
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const run = async (args) => {
|
|
45
|
-
const q = String((args && args.query) || '').trim();
|
|
46
|
-
if (!q) return '需要提供 query(要咨询平台的自然语言问题 / 输入)';
|
|
47
|
-
try {
|
|
48
|
-
// 走平台伪模型 → 服务端编排;client/sessionId 使这轮咨询也计入对话审计
|
|
49
|
-
const r = await streamChat({
|
|
50
|
-
baseUrl: config.baseUrl,
|
|
51
|
-
apiKey: config.apiKey,
|
|
52
|
-
model: ref,
|
|
53
|
-
messages: [{ role: 'user', content: q }],
|
|
54
|
-
client: config.client,
|
|
55
|
-
sessionId: config.sessionId,
|
|
56
|
-
}, {});
|
|
57
|
-
return r.content || '(平台无返回)';
|
|
58
|
-
} catch (e) {
|
|
59
|
-
if (e && e.name === 'AbortError') throw e;
|
|
60
|
-
return `平台调用失败:${e.message}`;
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
return { readOnly: true, spec, run };
|
|
65
|
-
}
|
|
1
|
+
import{streamChat as t}from"./api.js";const r=/^(agent|workflow|datahuman)-/;export function makeConsultTool({config:e}){const n=e&&e.consult;if(!n||!r.test(String(n)))return null;const o=function(t){return t.startsWith("workflow-")?"工作流":t.startsWith("datahuman-")?"业务数字人":"智能体"}(String(n));return{readOnly:!0,spec:{type:"function",function:{name:"consult_platform",description:`咨询平台${o}「${e.consultLabel||n}」:由平台侧编排作答(带其服务端知识库 RAG / 工具 / 联网检索)。遇到它擅长领域的问题优先调用它获取权威答复,再用本机工具把结论落地到项目。可多次调用。`,parameters:{type:"object",properties:{query:{type:"string",description:"自然语言的咨询 / 查询 / 运行输入"}},required:["query"]}}},run:async r=>{const o=String(r&&r.query||"").trim();if(!o)return"需要提供 query(要咨询平台的自然语言问题 / 输入)";try{return(await t({baseUrl:e.baseUrl,apiKey:e.apiKey,model:n,messages:[{role:"user",content:o}],client:e.client,sessionId:e.sessionId},{})).content||"(平台无返回)"}catch(t){if(t&&"AbortError"===t.name)throw t;return`平台调用失败:${t.message}`}}}}
|
package/src/context-report.js
CHANGED
|
@@ -1,119 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 用 buildModelMessages 取真实模型视图(含非破坏式压缩摘要),故与实际发送口径一致。
|
|
3
|
-
// token 为估算(CJK≈1.6/字,其余≈1/4 字符);若有上一轮实测 prompt_tokens,则整体按实测缩放校准。
|
|
4
|
-
|
|
5
|
-
import { buildModelMessages } from './compaction.js';
|
|
6
|
-
import { c } from './colors.js';
|
|
7
|
-
|
|
8
|
-
const CJK_RE = /[-ヿ㐀-鿿가-豈--]/g;
|
|
9
|
-
|
|
10
|
-
/** 估算文本 token 数(中英混排启发式)。 */
|
|
11
|
-
export function estimateTokens(text) {
|
|
12
|
-
if (!text) return 0;
|
|
13
|
-
const s = String(text);
|
|
14
|
-
const cjk = (s.match(CJK_RE) || []).length;
|
|
15
|
-
const other = s.length - cjk;
|
|
16
|
-
return Math.ceil(cjk * 1.6 + other / 4);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function msgChars(m) {
|
|
20
|
-
let n = 0;
|
|
21
|
-
if (typeof m.content === 'string') n += m.content.length;
|
|
22
|
-
else if (Array.isArray(m.content)) {
|
|
23
|
-
for (const part of m.content) {
|
|
24
|
-
if (part?.type === 'text' && part.text) n += part.text.length;
|
|
25
|
-
else if (part?.type === 'image_url') n += 800; // 多模态图片按占位近似
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return n;
|
|
29
|
-
}
|
|
30
|
-
function tokOf(m) {
|
|
31
|
-
if (typeof m.content === 'string') return estimateTokens(m.content);
|
|
32
|
-
if (Array.isArray(m.content)) {
|
|
33
|
-
let t = 0;
|
|
34
|
-
for (const part of m.content) {
|
|
35
|
-
if (part?.type === 'text' && part.text) t += estimateTokens(part.text);
|
|
36
|
-
else if (part?.type === 'image_url') t += 300;
|
|
37
|
-
}
|
|
38
|
-
return t;
|
|
39
|
-
}
|
|
40
|
-
return 0;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function fmtTok(n) {
|
|
44
|
-
n = Math.round(Number(n) || 0);
|
|
45
|
-
return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function bar(pct, width = 14) {
|
|
49
|
-
const filled = Math.max(0, Math.min(width, Math.round(pct * width)));
|
|
50
|
-
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 生成上下文占用报告文本。
|
|
55
|
-
* @param {object} p
|
|
56
|
-
* @param {any[]} p.messages 完整会话消息(含 system)
|
|
57
|
-
* @param {object} [p.compaction] 压缩状态 {upTo,summary}
|
|
58
|
-
* @param {object[]} [p.specs] 本回合会随请求发送的工具规格(估其常驻开销)
|
|
59
|
-
* @param {number} [p.lastPromptTokens] 上一轮实测 prompt_tokens(用于校准)
|
|
60
|
-
* @param {number} [p.autoCompactTokens] 自动压缩触发阈值
|
|
61
|
-
* @param {boolean} [p.platform] 平台伪模型(无本机工具/无压缩),仅粗统计
|
|
62
|
-
*/
|
|
63
|
-
export function contextReport({ messages, compaction, specs, lastPromptTokens, autoCompactTokens, platform }) {
|
|
64
|
-
const view = platform ? messages : buildModelMessages(messages, compaction || {});
|
|
65
|
-
const hasSummary = !!(compaction && compaction.summary && compaction.upTo > 1);
|
|
66
|
-
|
|
67
|
-
const buckets = {
|
|
68
|
-
system: { label: '系统提示词', tok: 0 },
|
|
69
|
-
tools: { label: '工具定义', tok: 0 },
|
|
70
|
-
summary: { label: '压缩摘要', tok: 0 },
|
|
71
|
-
user: { label: '用户消息', tok: 0 },
|
|
72
|
-
assistant: { label: '助手回复', tok: 0 },
|
|
73
|
-
toolcall: { label: '工具调用', tok: 0 },
|
|
74
|
-
toolresult: { label: '工具结果', tok: 0 },
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
view.forEach((m, i) => {
|
|
78
|
-
if (i === 0 && m.role === 'system') { buckets.system.tok += tokOf(m); return; }
|
|
79
|
-
if (i === 1 && hasSummary && m.role === 'user') { buckets.summary.tok += tokOf(m); return; }
|
|
80
|
-
if (m.role === 'user') buckets.user.tok += tokOf(m);
|
|
81
|
-
else if (m.role === 'assistant') {
|
|
82
|
-
buckets.assistant.tok += tokOf(m);
|
|
83
|
-
if (m.tool_calls) buckets.toolcall.tok += estimateTokens(JSON.stringify(m.tool_calls));
|
|
84
|
-
} else if (m.role === 'tool') buckets.toolresult.tok += tokOf(m);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
if (specs && specs.length) buckets.tools.tok = estimateTokens(JSON.stringify(specs));
|
|
88
|
-
|
|
89
|
-
const order = ['system', 'tools', 'summary', 'user', 'assistant', 'toolcall', 'toolresult'];
|
|
90
|
-
let total = order.reduce((s, k) => s + buckets[k].tok, 0);
|
|
91
|
-
|
|
92
|
-
// 有实测值则整体缩放校准(让分类合计对齐真实 prompt_tokens)
|
|
93
|
-
let scale = 1;
|
|
94
|
-
if (lastPromptTokens > 0 && total > 0) scale = lastPromptTokens / total;
|
|
95
|
-
const scaled = (k) => buckets[k].tok * scale;
|
|
96
|
-
const scaledTotal = lastPromptTokens > 0 ? lastPromptTokens : total;
|
|
97
|
-
|
|
98
|
-
const lines = [];
|
|
99
|
-
lines.push(c.bold('上下文占用') + c.gray('(当前发给模型的视图 · 估算)'));
|
|
100
|
-
for (const k of order) {
|
|
101
|
-
const t = scaled(k);
|
|
102
|
-
if (t < 1 && buckets[k].tok === 0) continue;
|
|
103
|
-
const pct = scaledTotal > 0 ? t / scaledTotal : 0;
|
|
104
|
-
lines.push(' ' + buckets[k].label.padEnd(6, ' ')
|
|
105
|
-
+ ' ' + ('~' + fmtTok(t)).padStart(7)
|
|
106
|
-
+ ' ' + c.cyan(bar(pct)) + ' ' + String(Math.round(pct * 100)).padStart(3) + '%');
|
|
107
|
-
}
|
|
108
|
-
lines.push(c.gray(' ' + '─'.repeat(34)));
|
|
109
|
-
|
|
110
|
-
const thr = Number(autoCompactTokens) || 80000;
|
|
111
|
-
const towardPct = Math.min(100, Math.round((scaledTotal / thr) * 100));
|
|
112
|
-
lines.push(' 合计 ' + ('~' + fmtTok(scaledTotal)).padStart(7)
|
|
113
|
-
+ c.gray(` 自动压缩阈值 ${fmtTok(thr)}(${towardPct}%)`));
|
|
114
|
-
if (lastPromptTokens > 0) lines.push(c.gray(` 实测上一轮 prompt_tokens:${fmtTok(lastPromptTokens)}(分类按此校准)`));
|
|
115
|
-
else lines.push(c.gray(' (尚无实测值,纯估算;发生一轮对话后更准)'));
|
|
116
|
-
if (hasSummary) lines.push(c.gray(' 已启用上下文压缩:更早的历史以摘要形式计入,原文完整保留。'));
|
|
117
|
-
lines.push(c.gray(' /compact 立即压缩 · /context 再次查看'));
|
|
118
|
-
return lines.join('\n');
|
|
119
|
-
}
|
|
1
|
+
import{buildModelMessages as t}from"./compaction.js";import{c as o}from"./colors.js";const e=/[-ヿ㐀-鿿가-豈--]/g;export function estimateTokens(t){if(!t)return 0;const o=String(t),s=(o.match(e)||[]).length,r=o.length-s;return Math.ceil(1.6*s+r/4)}function s(t){if("string"==typeof t.content)return estimateTokens(t.content);if(Array.isArray(t.content)){let o=0;for(const e of t.content)"text"===e?.type&&e.text?o+=estimateTokens(e.text):"image_url"===e?.type&&(o+=300);return o}return 0}function r(t){return(t=Math.round(Number(t)||0))>=1e3?(t/1e3).toFixed(1)+"k":String(t)}function n(t,o=14){const e=Math.max(0,Math.min(o,Math.round(t*o)));return"█".repeat(e)+"░".repeat(o-e)}export function contextReport({messages:e,compaction:a,specs:l,lastPromptTokens:u,autoCompactTokens:c,platform:i}){const m=i?e:t(e,a||{}),p=!!(a&&a.summary&&a.upTo>1),k={system:{label:"系统提示词",tok:0},tools:{label:"工具定义",tok:0},summary:{label:"压缩摘要",tok:0},user:{label:"用户消息",tok:0},assistant:{label:"助手回复",tok:0},toolcall:{label:"工具调用",tok:0},toolresult:{label:"工具结果",tok:0}};m.forEach((t,o)=>{0!==o||"system"!==t.role?1===o&&p&&"user"===t.role?k.summary.tok+=s(t):"user"===t.role?k.user.tok+=s(t):"assistant"===t.role?(k.assistant.tok+=s(t),t.tool_calls&&(k.toolcall.tok+=estimateTokens(JSON.stringify(t.tool_calls)))):"tool"===t.role&&(k.toolresult.tok+=s(t)):k.system.tok+=s(t)}),l&&l.length&&(k.tools.tok=estimateTokens(JSON.stringify(l)));const y=["system","tools","summary","user","assistant","toolcall","toolresult"];let h=y.reduce((t,o)=>t+k[o].tok,0),f=1;u>0&&h>0&&(f=u/h);const g=t=>k[t].tok*f,d=u>0?u:h,b=[];b.push(o.bold("上下文占用")+o.gray("(当前发给模型的视图 · 估算)"));for(const t of y){const e=g(t);if(e<1&&0===k[t].tok)continue;const s=d>0?e/d:0;b.push(" "+k[t].label.padEnd(6," ")+" "+("~"+r(e)).padStart(7)+" "+o.cyan(n(s))+" "+String(Math.round(100*s)).padStart(3)+"%")}b.push(o.gray(" "+"─".repeat(34)));const x=Number(c)||8e4,M=Math.min(100,Math.round(d/x*100));return b.push(" 合计 "+("~"+r(d)).padStart(7)+o.gray(` 自动压缩阈值 ${r(x)}(${M}%)`)),u>0?b.push(o.gray(` 实测上一轮 prompt_tokens:${r(u)}(分类按此校准)`)):b.push(o.gray(" (尚无实测值,纯估算;发生一轮对话后更准)")),p&&b.push(o.gray(" 已启用上下文压缩:更早的历史以摘要形式计入,原文完整保留。")),b.push(o.gray(" /compact 立即压缩 · /context 再次查看")),b.join("\n")}
|
package/src/context.js
CHANGED
|
@@ -1,152 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// + 项目说明文件(HUICAI.md/AGENTS.md/CLAUDE.md)+ 全局用户说明(~/.huicai/HUICAI.md)。
|
|
3
|
-
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import os from 'node:os';
|
|
7
|
-
import { execSync } from 'node:child_process';
|
|
8
|
-
import { loadMemory } from './memory.js';
|
|
9
|
-
import { skillCatalog } from './skills.js';
|
|
10
|
-
import { buildRepoMap } from './codeindex.js';
|
|
11
|
-
|
|
12
|
-
function git(cwd) {
|
|
13
|
-
const run = (cmd) => execSync(cmd, { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
14
|
-
try {
|
|
15
|
-
const branch = run('git rev-parse --abbrev-ref HEAD');
|
|
16
|
-
let status = '';
|
|
17
|
-
try {
|
|
18
|
-
status = run('git status --short');
|
|
19
|
-
} catch {
|
|
20
|
-
/* ignore */
|
|
21
|
-
}
|
|
22
|
-
return { branch, status };
|
|
23
|
-
} catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function topLevel(cwd) {
|
|
29
|
-
try {
|
|
30
|
-
return fs
|
|
31
|
-
.readdirSync(cwd, { withFileTypes: true })
|
|
32
|
-
.filter((e) => e.name !== '.git' && e.name !== 'node_modules')
|
|
33
|
-
.slice(0, 60)
|
|
34
|
-
.map((e) => (e.isDirectory() ? e.name + '/' : e.name))
|
|
35
|
-
.join(' ');
|
|
36
|
-
} catch {
|
|
37
|
-
return '';
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function projectDoc(cwd) {
|
|
42
|
-
for (const name of ['HUICAI.md', 'AGENTS.md', 'CLAUDE.md']) {
|
|
43
|
-
try {
|
|
44
|
-
const s = fs.readFileSync(path.join(cwd, name), 'utf8');
|
|
45
|
-
if (s.trim()) return { name, content: s.slice(0, 8000) };
|
|
46
|
-
} catch {
|
|
47
|
-
/* next */
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** 全局用户说明(~/.huicai/HUICAI.md):跨项目的个人偏好/约定。 */
|
|
54
|
-
function globalDoc() {
|
|
55
|
-
try {
|
|
56
|
-
const s = fs.readFileSync(path.join(os.homedir(), '.huicai', 'HUICAI.md'), 'utf8');
|
|
57
|
-
return s.trim() ? s.slice(0, 4000) : null;
|
|
58
|
-
} catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const GUIDELINES = [
|
|
64
|
-
'- 先了解现状再动手:并行使用 read_file/glob/grep/list_files 收集信息(一轮可同时调多个只读工具),不要臆测文件内容。',
|
|
65
|
-
'- read_file 输出的「行号→」前缀仅供定位,不是文件内容;写 edit_file 的 old_string/new_string 时绝不能包含行号前缀。',
|
|
66
|
-
'- 多步骤任务先用 todo_write 列出计划,并随进度更新状态(同一时刻至多一项 in_progress);单步小事不必用。',
|
|
67
|
-
'- 改代码优先用 edit_file 做最小精确改动;新建文件才用 write_file。遵循项目既有风格与命名。',
|
|
68
|
-
'- 用 run_bash 执行构建/测试/git 等;改动后尽量运行测试或构建自检,失败要定位修复而不是忽略。',
|
|
69
|
-
'- 需要在线资料(文档/API 说明)可用 fetch_url;带明确目的使用,不要漫游。',
|
|
70
|
-
'- 破坏性或不可逆操作(删除、强推、重置等)前先说明意图;写/执行类工具会请用户授权,被拒绝时据反馈调整方案。',
|
|
71
|
-
'- 不要把密钥、令牌等敏感值回显到输出或写进代码。',
|
|
72
|
-
'- 简洁直接,用中文回答;完成后用一两句话总结改了什么、如何验证的。',
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* 组装系统提示词。
|
|
77
|
-
* @param {string} cwd 工作目录
|
|
78
|
-
* @param {{persona?:string, append?:string}} [opts]
|
|
79
|
-
* persona 非空时以该智能体人格领衔(--agent 模式),但保留本机工具/环境说明;
|
|
80
|
-
* append 追加在末尾(--append-system-prompt)。
|
|
81
|
-
*/
|
|
82
|
-
export function buildSystemPrompt(cwd, opts = {}) {
|
|
83
|
-
const g = git(cwd);
|
|
84
|
-
const doc = projectDoc(cwd);
|
|
85
|
-
const gdoc = globalDoc();
|
|
86
|
-
let p = '';
|
|
87
|
-
if (opts.persona && opts.persona.trim()) {
|
|
88
|
-
p += '# 你的角色设定\n' + opts.persona.trim() + '\n\n';
|
|
89
|
-
p += '# 运行方式\n';
|
|
90
|
-
p += '你现在运行在用户终端里,可通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令来真正完成任务。请在保持上述角色设定的前提下遵循以下准则:\n';
|
|
91
|
-
} else {
|
|
92
|
-
p += '你是 huicai,一个运行在用户终端里的专业自治编码与自动化智能体。\n';
|
|
93
|
-
p += '你能通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令,从而真正完成任务,而不仅是给建议。\n\n';
|
|
94
|
-
p += '# 工作准则\n';
|
|
95
|
-
}
|
|
96
|
-
p += GUIDELINES.join('\n') + '\n\n';
|
|
97
|
-
p += '# 运行环境\n';
|
|
98
|
-
p += `- 工作目录: ${cwd}\n`;
|
|
99
|
-
p += `- 操作系统: ${os.platform()} ${os.release()} (${os.arch()})\n`;
|
|
100
|
-
p += `- 日期: ${new Date().toISOString().slice(0, 10)}\n`;
|
|
101
|
-
if (g) {
|
|
102
|
-
p += `- Git 分支: ${g.branch}\n`;
|
|
103
|
-
if (g.status) {
|
|
104
|
-
const lines = g.status.split('\n').slice(0, 30).map((l) => ' ' + l).join('\n');
|
|
105
|
-
p += `- 未提交改动:\n${lines}\n`;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const top = topLevel(cwd);
|
|
109
|
-
if (top) p += `- 顶层条目: ${top}\n`;
|
|
110
|
-
const editor = editorContext();
|
|
111
|
-
if (editor) p += '\n# 编辑器上下文(来自 IDE 集成)\n' + editor;
|
|
112
|
-
if (gdoc) p += `\n# 全局用户说明(~/.huicai/HUICAI.md)\n${gdoc}\n`;
|
|
113
|
-
if (doc) p += `\n# 项目说明(${doc.name})\n${doc.content}\n`;
|
|
114
|
-
// 项目地图:关键文件与主要符号的紧凑概览,帮智能体一开局就建立全局观(用 search_code 按概念深入)
|
|
115
|
-
if (opts.repoMap !== false) {
|
|
116
|
-
try {
|
|
117
|
-
const repoMap = buildRepoMap(cwd);
|
|
118
|
-
if (repoMap) p += `\n# 项目地图(关键文件与主要符号;用 search_code 按概念检索相关实现)\n${repoMap}\n`;
|
|
119
|
-
} catch { /* 大仓库/权限问题:跳过地图,不影响对话 */ }
|
|
120
|
-
}
|
|
121
|
-
// 本地技能目录(渐进披露:仅名称+描述常驻,正文经 load_skill 按需载入)
|
|
122
|
-
const skills = skillCatalog(cwd);
|
|
123
|
-
if (skills) p += `\n# 可用技能(本地 Agent Skills)\n${skills}\n`;
|
|
124
|
-
if (opts.memory !== false) {
|
|
125
|
-
const mem = loadMemory(cwd);
|
|
126
|
-
if (mem) {
|
|
127
|
-
p += `\n# 长期记忆(此前会话经 memory_write 记录,跨会话生效)\n${mem}\n`;
|
|
128
|
-
p += '(发现新的、值得跨会话记住的稳定事实/偏好/结论时,用 memory_write 记录。)\n';
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (opts.append && String(opts.append).trim()) p += '\n# 附加指示\n' + String(opts.append).trim() + '\n';
|
|
132
|
-
return p;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** VS Code 等 IDE 扩展经环境变量注入的当前编辑上下文:正在编辑的文件 + 选中片段 + 诊断(Problems)。 */
|
|
136
|
-
function editorContext() {
|
|
137
|
-
const file = process.env.HUICAI_IDE_FILE;
|
|
138
|
-
const selection = process.env.HUICAI_IDE_SELECTION;
|
|
139
|
-
const diagnostics = process.env.HUICAI_IDE_DIAGNOSTICS;
|
|
140
|
-
if (!file && !selection && !diagnostics) return '';
|
|
141
|
-
let s = '';
|
|
142
|
-
if (file) s += `- 用户当前正在编辑的文件: ${file}\n`;
|
|
143
|
-
if (selection) {
|
|
144
|
-
const clipped = selection.length > 4000 ? selection.slice(0, 4000) + '\n…(选区已截断)' : selection;
|
|
145
|
-
s += '- 用户当前选中的代码片段:\n```\n' + clipped + '\n```\n';
|
|
146
|
-
}
|
|
147
|
-
if (diagnostics) {
|
|
148
|
-
const clipped = diagnostics.length > 4000 ? diagnostics.slice(0, 4000) + '\n…(诊断已截断)' : diagnostics;
|
|
149
|
-
s += '- 编辑器诊断(错误/警告,来自 Problems 面板):\n```\n' + clipped + '\n```\n';
|
|
150
|
-
}
|
|
151
|
-
return s;
|
|
152
|
-
}
|
|
1
|
+
import n from"node:fs";import e from"node:path";import t from"node:os";import{execSync as r}from"node:child_process";import{loadMemory as i}from"./memory.js";import{skillCatalog as o}from"./skills.js";import{buildRepoMap as s}from"./codeindex.js";const c=["- 先了解现状再动手:并行使用 read_file/glob/grep/list_files 收集信息(一轮可同时调多个只读工具),不要臆测文件内容。","- read_file 输出的「行号→」前缀仅供定位,不是文件内容;写 edit_file 的 old_string/new_string 时绝不能包含行号前缀。","- 多步骤任务先用 todo_write 列出计划,并随进度更新状态(同一时刻至多一项 in_progress);单步小事不必用。","- 改代码优先用 edit_file 做最小精确改动;新建文件才用 write_file。遵循项目既有风格与命名。","- 用 run_bash 执行构建/测试/git 等;改动后尽量运行测试或构建自检,失败要定位修复而不是忽略。","- 需要在线资料(文档/API 说明)可用 fetch_url;带明确目的使用,不要漫游。","- 破坏性或不可逆操作(删除、强推、重置等)前先说明意图;写/执行类工具会请用户授权,被拒绝时据反馈调整方案。","- 不要把密钥、令牌等敏感值回显到输出或写进代码。","- 简洁直接,用中文回答;完成后用一两句话总结改了什么、如何验证的。"];export function buildSystemPrompt(l,m={}){const a=function(n){const e=e=>r(e,{cwd:n,stdio:["ignore","pipe","ignore"]}).toString().trim();try{const n=e("git rev-parse --abbrev-ref HEAD");let t="";try{t=e("git status --short")}catch{}return{branch:n,status:t}}catch{return null}}(l),f=function(t){for(const r of["HUICAI.md","AGENTS.md","CLAUDE.md"])try{const i=n.readFileSync(e.join(t,r),"utf8");if(i.trim())return{name:r,content:i.slice(0,8e3)}}catch{}return null}(l),p=function(){try{const r=n.readFileSync(e.join(t.homedir(),".huicai","HUICAI.md"),"utf8");return r.trim()?r.slice(0,4e3):null}catch{return null}}();let u="";if(m.persona&&m.persona.trim()?(u+="# 你的角色设定\n"+m.persona.trim()+"\n\n",u+="# 运行方式\n",u+="你现在运行在用户终端里,可通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令来真正完成任务。请在保持上述角色设定的前提下遵循以下准则:\n"):(u+="你是 huicai,一个运行在用户终端里的专业自治编码与自动化智能体。\n",u+="你能通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令,从而真正完成任务,而不仅是给建议。\n\n",u+="# 工作准则\n"),u+=c.join("\n")+"\n\n",u+="# 运行环境\n",u+=`- 工作目录: ${l}\n`,u+=`- 操作系统: ${t.platform()} ${t.release()} (${t.arch()})\n`,u+=`- 日期: ${(new Date).toISOString().slice(0,10)}\n`,a&&(u+=`- Git 分支: ${a.branch}\n`,a.status)){u+=`- 未提交改动:\n${a.status.split("\n").slice(0,30).map(n=>" "+n).join("\n")}\n`}const d=function(e){try{return n.readdirSync(e,{withFileTypes:!0}).filter(n=>".git"!==n.name&&"node_modules"!==n.name).slice(0,60).map(n=>n.isDirectory()?n.name+"/":n.name).join(" ")}catch{return""}}(l);d&&(u+=`- 顶层条目: ${d}\n`);const h=function(){const n=process.env.HUICAI_IDE_FILE,e=process.env.HUICAI_IDE_SELECTION,t=process.env.HUICAI_IDE_DIAGNOSTICS;if(!n&&!e&&!t)return"";let r="";n&&(r+=`- 用户当前正在编辑的文件: ${n}\n`);if(e){r+="- 用户当前选中的代码片段:\n```\n"+(e.length>4e3?e.slice(0,4e3)+"\n…(选区已截断)":e)+"\n```\n"}if(t){r+="- 编辑器诊断(错误/警告,来自 Problems 面板):\n```\n"+(t.length>4e3?t.slice(0,4e3)+"\n…(诊断已截断)":t)+"\n```\n"}return r}();if(h&&(u+="\n# 编辑器上下文(来自 IDE 集成)\n"+h),p&&(u+=`\n# 全局用户说明(~/.huicai/HUICAI.md)\n${p}\n`),f&&(u+=`\n# 项目说明(${f.name})\n${f.content}\n`),!1!==m.repoMap)try{const n=s(l);n&&(u+=`\n# 项目地图(关键文件与主要符号;用 search_code 按概念检索相关实现)\n${n}\n`)}catch{}const _=o(l);if(_&&(u+=`\n# 可用技能(本地 Agent Skills)\n${_}\n`),!1!==m.memory){const n=i(l);n&&(u+=`\n# 长期记忆(此前会话经 memory_write 记录,跨会话生效)\n${n}\n`,u+="(发现新的、值得跨会话记住的稳定事实/偏好/结论时,用 memory_write 记录。)\n")}return m.append&&String(m.append).trim()&&(u+="\n# 附加指示\n"+String(m.append).trim()+"\n"),u}
|