chatccc 0.2.63 → 0.2.64
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/config.sample.json +1 -2
- package/im-skills/feishu-skill/download-video.mjs +10 -17
- package/im-skills/wechat-file-skill/receive-send-file.md +8 -11
- package/im-skills/wechat-file-skill/send-file.mjs +7 -24
- package/im-skills/wechat-file-skill/skill.md +4 -4
- package/im-skills/wechat-video-skill/receive-send-video.md +7 -9
- package/im-skills/wechat-video-skill/send-video.mjs +4 -8
- package/im-skills/wechat-video-skill/skill.md +4 -4
- package/package.json +59 -59
- package/src/__tests__/agent-platform-routing.test.ts +26 -0
- package/src/__tests__/claude-adapter.test.ts +48 -48
- package/src/__tests__/config-reload.test.ts +14 -14
- package/src/__tests__/config-sample.test.ts +22 -22
- package/src/__tests__/im-skills.test.ts +56 -2
- package/src/__tests__/privacy.test.ts +142 -142
- package/src/__tests__/simplify.test.ts +282 -282
- package/src/__tests__/web-ui.test.ts +23 -23
- package/src/adapters/claude-adapter.ts +40 -40
- package/src/agent-file-rpc.ts +19 -4
- package/src/agent-image-rpc.ts +19 -4
- package/src/agent-platform-routing.ts +28 -0
- package/src/config.ts +2 -24
- package/src/im-skills.ts +9 -0
- package/src/index.ts +11 -6
- package/src/privacy.ts +67 -67
- package/src/session.ts +14 -2
- package/src/simplify.ts +119 -119
- package/src/web-ui.ts +3 -26
package/src/simplify.ts
CHANGED
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { resolve as resolvePath } from "node:path";
|
|
3
|
-
import { PROJECT_ROOT } from "./config.ts";
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// 消息简化规则 —— 数据驱动,在 simplify.json 中配置
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
interface ToolRule {
|
|
10
|
-
template: string;
|
|
11
|
-
maxLength: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface SimplifyConfig {
|
|
15
|
-
tool_use?: Record<string, ToolRule>;
|
|
16
|
-
tool_result?: Record<string, ToolRule>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let config: SimplifyConfig | null = null;
|
|
20
|
-
let loaded = false;
|
|
21
|
-
|
|
22
|
-
function loadConfig(): SimplifyConfig {
|
|
23
|
-
const filePath = resolvePath(PROJECT_ROOT, "simplify.json");
|
|
24
|
-
if (!existsSync(filePath)) return {};
|
|
25
|
-
try {
|
|
26
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
27
|
-
const parsed = JSON.parse(raw);
|
|
28
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
29
|
-
console.error("[SIMPLIFY] simplify.json 格式错误:应为对象");
|
|
30
|
-
return {};
|
|
31
|
-
}
|
|
32
|
-
return parsed as SimplifyConfig;
|
|
33
|
-
} catch (err) {
|
|
34
|
-
console.error(`[SIMPLIFY] 读取 simplify.json 失败: ${(err as Error).message}`);
|
|
35
|
-
return {};
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function getConfig(): SimplifyConfig {
|
|
40
|
-
if (!loaded) {
|
|
41
|
-
config = loadConfig();
|
|
42
|
-
loaded = true;
|
|
43
|
-
}
|
|
44
|
-
return config!;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** 热重载 */
|
|
48
|
-
export function reloadSimplifyConfig(): void {
|
|
49
|
-
loaded = false;
|
|
50
|
-
config = null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 对模板字符串中的 {field} 占位符做替换。
|
|
55
|
-
* fields 为可用字段映射,extra 是额外的上下文(如 tool_use_id 的 id)。
|
|
56
|
-
*/
|
|
57
|
-
function resolveTemplate(template: string, fields: Record<string, unknown>, extra?: Record<string, string>): string {
|
|
58
|
-
let result = template;
|
|
59
|
-
if (extra) {
|
|
60
|
-
for (const [k, v] of Object.entries(extra)) {
|
|
61
|
-
result = result.split(`{${k}}`).join(v);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
for (const [k, v] of Object.entries(fields)) {
|
|
65
|
-
const strVal = typeof v === "string" ? v : JSON.stringify(v);
|
|
66
|
-
result = result.split(`{${k}}`).join(strVal);
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* 简化 tool_use 展示。
|
|
73
|
-
* 返回 null 表示无规则,调用方应回退到默认格式化。
|
|
74
|
-
*/
|
|
75
|
-
export function simplifyToolUse(name: string, input: unknown): string | null {
|
|
76
|
-
const cfg = getConfig();
|
|
77
|
-
const rules = cfg.tool_use;
|
|
78
|
-
if (!rules) return null;
|
|
79
|
-
const rule = rules[name];
|
|
80
|
-
if (!rule) return null;
|
|
81
|
-
|
|
82
|
-
const fields = typeof input === "object" && input !== null
|
|
83
|
-
? input as Record<string, unknown>
|
|
84
|
-
: {};
|
|
85
|
-
let result = resolveTemplate(rule.template, fields);
|
|
86
|
-
if (result.length > rule.maxLength) {
|
|
87
|
-
result = result.slice(0, rule.maxLength) + "...";
|
|
88
|
-
}
|
|
89
|
-
return result;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* 简化 tool_result 展示。
|
|
94
|
-
* 返回 null 表示无规则,调用方应回退到默认格式化。
|
|
95
|
-
* toolCallInput 为对应的 tool_use 输入(可选),用于在 result 模板中引用输入字段。
|
|
96
|
-
*/
|
|
97
|
-
export function simplifyToolResult(
|
|
98
|
-
name: string,
|
|
99
|
-
toolUseId: string,
|
|
100
|
-
isError: boolean,
|
|
101
|
-
toolCallInput?: unknown,
|
|
102
|
-
): string | null {
|
|
103
|
-
const cfg = getConfig();
|
|
104
|
-
const rules = cfg.tool_result;
|
|
105
|
-
if (!rules) return null;
|
|
106
|
-
const rule = rules[name];
|
|
107
|
-
if (!rule) return null;
|
|
108
|
-
|
|
109
|
-
const id = toolUseId.slice(-6);
|
|
110
|
-
const extra = { id };
|
|
111
|
-
const fields = toolCallInput && typeof toolCallInput === "object"
|
|
112
|
-
? toolCallInput as Record<string, unknown>
|
|
113
|
-
: {};
|
|
114
|
-
let result = resolveTemplate(rule.template, fields, extra);
|
|
115
|
-
if (isError) result = "❌ " + result;
|
|
116
|
-
if (result.length > rule.maxLength) {
|
|
117
|
-
result = result.slice(0, rule.maxLength) + "...";
|
|
118
|
-
}
|
|
119
|
-
return result;
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
import { PROJECT_ROOT } from "./config.ts";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// 消息简化规则 —— 数据驱动,在 simplify.json 中配置
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
interface ToolRule {
|
|
10
|
+
template: string;
|
|
11
|
+
maxLength: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SimplifyConfig {
|
|
15
|
+
tool_use?: Record<string, ToolRule>;
|
|
16
|
+
tool_result?: Record<string, ToolRule>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let config: SimplifyConfig | null = null;
|
|
20
|
+
let loaded = false;
|
|
21
|
+
|
|
22
|
+
function loadConfig(): SimplifyConfig {
|
|
23
|
+
const filePath = resolvePath(PROJECT_ROOT, "simplify.json");
|
|
24
|
+
if (!existsSync(filePath)) return {};
|
|
25
|
+
try {
|
|
26
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
27
|
+
const parsed = JSON.parse(raw);
|
|
28
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
29
|
+
console.error("[SIMPLIFY] simplify.json 格式错误:应为对象");
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
return parsed as SimplifyConfig;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(`[SIMPLIFY] 读取 simplify.json 失败: ${(err as Error).message}`);
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getConfig(): SimplifyConfig {
|
|
40
|
+
if (!loaded) {
|
|
41
|
+
config = loadConfig();
|
|
42
|
+
loaded = true;
|
|
43
|
+
}
|
|
44
|
+
return config!;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 热重载 */
|
|
48
|
+
export function reloadSimplifyConfig(): void {
|
|
49
|
+
loaded = false;
|
|
50
|
+
config = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 对模板字符串中的 {field} 占位符做替换。
|
|
55
|
+
* fields 为可用字段映射,extra 是额外的上下文(如 tool_use_id 的 id)。
|
|
56
|
+
*/
|
|
57
|
+
function resolveTemplate(template: string, fields: Record<string, unknown>, extra?: Record<string, string>): string {
|
|
58
|
+
let result = template;
|
|
59
|
+
if (extra) {
|
|
60
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
61
|
+
result = result.split(`{${k}}`).join(v);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
65
|
+
const strVal = typeof v === "string" ? v : JSON.stringify(v);
|
|
66
|
+
result = result.split(`{${k}}`).join(strVal);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 简化 tool_use 展示。
|
|
73
|
+
* 返回 null 表示无规则,调用方应回退到默认格式化。
|
|
74
|
+
*/
|
|
75
|
+
export function simplifyToolUse(name: string, input: unknown): string | null {
|
|
76
|
+
const cfg = getConfig();
|
|
77
|
+
const rules = cfg.tool_use;
|
|
78
|
+
if (!rules) return null;
|
|
79
|
+
const rule = rules[name];
|
|
80
|
+
if (!rule) return null;
|
|
81
|
+
|
|
82
|
+
const fields = typeof input === "object" && input !== null
|
|
83
|
+
? input as Record<string, unknown>
|
|
84
|
+
: {};
|
|
85
|
+
let result = resolveTemplate(rule.template, fields);
|
|
86
|
+
if (result.length > rule.maxLength) {
|
|
87
|
+
result = result.slice(0, rule.maxLength) + "...";
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 简化 tool_result 展示。
|
|
94
|
+
* 返回 null 表示无规则,调用方应回退到默认格式化。
|
|
95
|
+
* toolCallInput 为对应的 tool_use 输入(可选),用于在 result 模板中引用输入字段。
|
|
96
|
+
*/
|
|
97
|
+
export function simplifyToolResult(
|
|
98
|
+
name: string,
|
|
99
|
+
toolUseId: string,
|
|
100
|
+
isError: boolean,
|
|
101
|
+
toolCallInput?: unknown,
|
|
102
|
+
): string | null {
|
|
103
|
+
const cfg = getConfig();
|
|
104
|
+
const rules = cfg.tool_result;
|
|
105
|
+
if (!rules) return null;
|
|
106
|
+
const rule = rules[name];
|
|
107
|
+
if (!rule) return null;
|
|
108
|
+
|
|
109
|
+
const id = toolUseId.slice(-6);
|
|
110
|
+
const extra = { id };
|
|
111
|
+
const fields = toolCallInput && typeof toolCallInput === "object"
|
|
112
|
+
? toolCallInput as Record<string, unknown>
|
|
113
|
+
: {};
|
|
114
|
+
let result = resolveTemplate(rule.template, fields, extra);
|
|
115
|
+
if (isError) result = "❌ " + result;
|
|
116
|
+
if (result.length > rule.maxLength) {
|
|
117
|
+
result = result.slice(0, rule.maxLength) + "...";
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
120
|
}
|
package/src/web-ui.ts
CHANGED
|
@@ -342,9 +342,6 @@ export function unflattenConfig(flat: Record<string, unknown>): Record<string, u
|
|
|
342
342
|
} else if (key === "CHATCCC_APP_SECRET") {
|
|
343
343
|
result.feishu = result.feishu || {};
|
|
344
344
|
(result.feishu as Record<string, unknown>).appSecret = val;
|
|
345
|
-
} else if (key === "CHATCCC_FEISHU_DOMAIN") {
|
|
346
|
-
result.feishu = result.feishu || {};
|
|
347
|
-
(result.feishu as Record<string, unknown>).domain = val;
|
|
348
345
|
} else if (key === "CHATCCC_FEISHU_ENABLED") {
|
|
349
346
|
result.platforms = result.platforms || {};
|
|
350
347
|
(result.platforms as Record<string, unknown>).feishu = (result.platforms as Record<string, unknown>).feishu || {};
|
|
@@ -642,14 +639,6 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
642
639
|
<input type="password" id="field-CHATCCC_APP_SECRET" placeholder="...">
|
|
643
640
|
<div class="hint">飞书开放平台「凭证与基础信息」→ App Secret</div>
|
|
644
641
|
</div>
|
|
645
|
-
<div class="form-group">
|
|
646
|
-
<label>平台域名</label>
|
|
647
|
-
<select id="field-CHATCCC_FEISHU_DOMAIN">
|
|
648
|
-
<option value="feishu">飞书 (open.feishu.cn)</option>
|
|
649
|
-
<option value="lark">Lark (open.larksuite.com)</option>
|
|
650
|
-
</select>
|
|
651
|
-
<div class="hint">国际版 Lark 用户请选择 Lark</div>
|
|
652
|
-
</div>
|
|
653
642
|
</div>
|
|
654
643
|
</div>
|
|
655
644
|
|
|
@@ -837,7 +826,6 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
837
826
|
</div>
|
|
838
827
|
<div class="config-row"><span class="key">App ID</span><span class="val" id="cfg-APP_ID">-</span></div>
|
|
839
828
|
<div class="config-row"><span class="key">App Secret</span><span class="val" id="cfg-APP_SECRET">-</span></div>
|
|
840
|
-
<div class="config-row"><span class="key">平台域名</span><span class="val" id="cfg-FEISHU_DOMAIN">-</span></div>
|
|
841
829
|
<button class="btn btn-outline" style="margin-top:8px" onclick="editSection('feishu')">编辑</button>
|
|
842
830
|
</div>
|
|
843
831
|
</details>
|
|
@@ -938,7 +926,7 @@ const AGENT_FIELDS = {
|
|
|
938
926
|
cursor: ['CHATCCC_CURSOR_PATH','CHATCCC_CURSOR_MODEL'],
|
|
939
927
|
codex: ['CHATCCC_CODEX_PATH','CHATCCC_CODEX_MODEL','CHATCCC_CODEX_EFFORT']
|
|
940
928
|
};
|
|
941
|
-
const FEISHU_FIELDS = ['CHATCCC_APP_ID','CHATCCC_APP_SECRET'
|
|
929
|
+
const FEISHU_FIELDS = ['CHATCCC_APP_ID','CHATCCC_APP_SECRET'];
|
|
942
930
|
|
|
943
931
|
// 当前选中的 Claude API 模式("official" / "thirdparty")
|
|
944
932
|
// Wizard / Dashboard 都通过这个变量驱动 UI 显隐和提交时的 mode 字段
|
|
@@ -1209,7 +1197,6 @@ function renderStep1() {
|
|
|
1209
1197
|
var f = c.feishu || {};
|
|
1210
1198
|
prefillNested('field-CHATCCC_APP_ID', f.appId);
|
|
1211
1199
|
prefillNested('field-CHATCCC_APP_SECRET', f.appSecret);
|
|
1212
|
-
prefillNested('field-CHATCCC_FEISHU_DOMAIN', f.domain || 'feishu');
|
|
1213
1200
|
// 平台开关:按已有 config 回填;首次配置(无飞书凭证)时默认关闭飞书、开启微信
|
|
1214
1201
|
var hasExistingCreds = Boolean(c.feishu?.appId?.trim() && c.feishu?.appSecret?.trim());
|
|
1215
1202
|
var feishuEnabled = hasExistingCreds
|
|
@@ -1362,7 +1349,6 @@ function renderStep3() {
|
|
|
1362
1349
|
if (state.platformsEnabled.feishu) {
|
|
1363
1350
|
lines.push('<div class="config-row"><span class="key">CHATCCC_APP_ID</span><span class="val">' + (vars.CHATCCC_APP_ID || '<span style="color:#ef4444">未填写</span>') + '</span></div>');
|
|
1364
1351
|
lines.push('<div class="config-row"><span class="key">CHATCCC_APP_SECRET</span><span class="val">' + (vars.CHATCCC_APP_SECRET ? '***已设置***' : '<span style="color:#ef4444">未填写</span>') + '</span></div>');
|
|
1365
|
-
lines.push('<div class="config-row"><span class="key">平台域名</span><span class="val">' + ((vars.CHATCCC_FEISHU_DOMAIN === 'lark') ? 'Lark (open.larksuite.com)' : '飞书 (open.feishu.cn)') + '</span></div>');
|
|
1366
1352
|
}
|
|
1367
1353
|
|
|
1368
1354
|
lines.push('<h3 style="margin:16px 0 8px">微信 iLink</h3>');
|
|
@@ -1548,7 +1534,6 @@ function updateDashboardUI() {
|
|
|
1548
1534
|
state.platformsEnabled.feishu = feishuEnabled;
|
|
1549
1535
|
document.getElementById('cfg-APP_ID').textContent = c.feishu && c.feishu.appId ? c.feishu.appId.slice(0,8) + '...' + c.feishu.appId.slice(-4) : '-';
|
|
1550
1536
|
document.getElementById('cfg-APP_SECRET').textContent = c.feishu && c.feishu.appSecret ? '***已设置***' : '-';
|
|
1551
|
-
document.getElementById('cfg-FEISHU_DOMAIN').textContent = (c.feishu && c.feishu.domain === 'lark') ? 'Lark (open.larksuite.com)' : '飞书 (open.feishu.cn)';
|
|
1552
1537
|
|
|
1553
1538
|
// 只显示已启用的 Agent 卡片(按 enabled 字段;缺省时退回到"任一字段非空"兼容旧 config)
|
|
1554
1539
|
var claudeOn = isAgentEnabled(c.claude, CLAUDE_FALLBACK_KEYS);
|
|
@@ -1651,7 +1636,7 @@ function editSection(section) {
|
|
|
1651
1636
|
|
|
1652
1637
|
var html = '';
|
|
1653
1638
|
var labelMap = {
|
|
1654
|
-
'CHATCCC_APP_ID': 'App ID', 'CHATCCC_APP_SECRET': 'App Secret',
|
|
1639
|
+
'CHATCCC_APP_ID': 'App ID', 'CHATCCC_APP_SECRET': 'App Secret',
|
|
1655
1640
|
'CLAUDE_API_KEY': 'API Key', 'CLAUDE_BASE_URL': 'Base URL',
|
|
1656
1641
|
'CHATCCC_ANTHROPIC_MODEL': '模型', 'CHATCCC_ANTHROPIC_SUBAGENT_MODEL': 'Subagent 模型', 'CHATCCC_ANTHROPIC_EFFORT': 'Effort',
|
|
1657
1642
|
'CHATCCC_CURSOR_PATH': 'CLI 路径', 'CHATCCC_CURSOR_MODEL': '模型',
|
|
@@ -1688,7 +1673,6 @@ function editSection(section) {
|
|
|
1688
1673
|
if (section === 'feishu') {
|
|
1689
1674
|
if (key === 'CHATCCC_APP_ID' && state.config.feishu) val = state.config.feishu.appId || '';
|
|
1690
1675
|
else if (key === 'CHATCCC_APP_SECRET' && state.config.feishu) val = state.config.feishu.appSecret || '';
|
|
1691
|
-
else if (key === 'CHATCCC_FEISHU_DOMAIN' && state.config.feishu) val = state.config.feishu.domain || 'feishu';
|
|
1692
1676
|
} else if (section === 'claude' && state.config.claude) {
|
|
1693
1677
|
if (key === 'CLAUDE_API_KEY') val = state.config.claude.apiKey || '';
|
|
1694
1678
|
else if (key === 'CLAUDE_BASE_URL') val = state.config.claude.baseUrl || '';
|
|
@@ -1723,14 +1707,7 @@ function editSection(section) {
|
|
|
1723
1707
|
var groupClass = 'form-group' + (isClaudeSubagentField && claudeApiMode !== 'thirdparty' ? ' hidden' : '');
|
|
1724
1708
|
var subagentAttr = isClaudeSubagentField ? ' data-claude-subagent-field="1"' : '';
|
|
1725
1709
|
html += '<div class="' + groupClass + '"' + subagentAttr + '><label>' + (labelMap[key] || key) + '</label>';
|
|
1726
|
-
|
|
1727
|
-
html += '<select id="edit-' + key + '">';
|
|
1728
|
-
html += '<option value="feishu"' + (val === 'lark' ? '' : ' selected') + '>飞书 (open.feishu.cn)</option>';
|
|
1729
|
-
html += '<option value="lark"' + (val === 'lark' ? ' selected' : '') + '>Lark (open.larksuite.com)</option>';
|
|
1730
|
-
html += '</select>';
|
|
1731
|
-
} else {
|
|
1732
|
-
html += '<input type="' + (isSecret ? 'password' : 'text') + '" id="edit-' + key + '" value="' + String(val).replace(/"/g,'"') + '">';
|
|
1733
|
-
}
|
|
1710
|
+
html += '<input type="' + (isSecret ? 'password' : 'text') + '" id="edit-' + key + '" value="' + String(val).replace(/"/g,'"') + '">';
|
|
1734
1711
|
html += '</div>';
|
|
1735
1712
|
});
|
|
1736
1713
|
|