chatccc 0.2.96 → 0.2.97
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 +1 -1
- package/src/__tests__/feishu-platform.test.ts +2 -1
- package/src/adapters/adapter-interface.ts +5 -0
- package/src/adapters/codex-adapter.ts +9 -3
- package/src/adapters/cursor-adapter.ts +19 -5
- package/src/cards.ts +14 -31
- package/src/config.ts +19 -0
- package/src/feishu-api.ts +126 -1
- package/src/feishu-platform.ts +5 -0
- package/src/index.ts +3 -1
- package/src/orchestrator.ts +105 -73
- package/src/session.ts +25 -7
- package/src/sim-platform.ts +6 -0
- package/src/sim-store.ts +1 -1
- package/src/stream-state.ts +36 -33
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { getPlatform, setPlatform, getTenantAccessToken, getChatInfo, createGroupChat, updateChatInfo, sendTextReply, sendCardReply, sendRawCard, extractSessionInfo, formatDelayNotice, reportPermissionResults, verifyAllPermissions, addReaction, recallMessage, updateCardMessage, setChatAvatar, disbandChat, sendRestartCard } from "../feishu-platform.ts";
|
|
2
|
+
import { getPlatform, setPlatform, getTenantAccessToken, getChatInfo, createGroupChat, updateChatInfo, sendTextReply, sendCardReply, sendRawCard, sendPostMessage, extractSessionInfo, formatDelayNotice, reportPermissionResults, verifyAllPermissions, addReaction, recallMessage, updateCardMessage, setChatAvatar, disbandChat, sendRestartCard } from "../feishu-platform.ts";
|
|
3
3
|
import type { FeishuPlatform } from "../feishu-platform.ts";
|
|
4
4
|
|
|
5
5
|
const realPlatform = getPlatform();
|
|
@@ -17,6 +17,7 @@ describe("feishu-platform", () => {
|
|
|
17
17
|
sendTextReply: async () => true,
|
|
18
18
|
sendCardReply: async () => true,
|
|
19
19
|
sendRawCard: async () => true,
|
|
20
|
+
sendPostMessage: async () => true,
|
|
20
21
|
sendImageReply: async () => true,
|
|
21
22
|
sendFileReply: async () => true,
|
|
22
23
|
addReaction: async () => {},
|
|
@@ -31,6 +31,11 @@ export interface UnifiedTextBlock {
|
|
|
31
31
|
* 由 pickFinalReply 在两者之间挑选。
|
|
32
32
|
*
|
|
33
33
|
* 对没有 partial/final 双轨的适配器(如 Claude SDK),永远不会 emit 此类型。
|
|
34
|
+
*
|
|
35
|
+
* 命名注意:此处的 "final" 指"完整/最终版本"(与 partial delta 相对),
|
|
36
|
+
* 不等于 stream-state / AccumulatorState 中的 finalReply / finalText。
|
|
37
|
+
* 后者命名含 "final" 但实际语义是"本轮所有文本的累积",属历史遗留命名,
|
|
38
|
+
* 详见 session.ts 的 AccumulatorState 注释和 stream-state.ts 的 StreamState 注释。
|
|
34
39
|
*/
|
|
35
40
|
export interface UnifiedTextFinalBlock {
|
|
36
41
|
type: "text_final";
|
|
@@ -150,9 +150,10 @@ function spawnCodex(
|
|
|
150
150
|
args: string[],
|
|
151
151
|
cwd?: string,
|
|
152
152
|
stdinText?: string,
|
|
153
|
+
modelOverride?: string,
|
|
153
154
|
): ChildProcess {
|
|
154
155
|
const allArgs = [...args];
|
|
155
|
-
const model = resolveCodexModel();
|
|
156
|
+
const model = modelOverride ?? resolveCodexModel();
|
|
156
157
|
if (model) {
|
|
157
158
|
// 把 -m 插在 exec 后面、其他参数前面
|
|
158
159
|
const execIdx = allArgs.indexOf("exec");
|
|
@@ -222,9 +223,11 @@ class CodexAdapter implements ToolAdapter {
|
|
|
222
223
|
readonly displayName = "Codex";
|
|
223
224
|
readonly sessionDescPrefix = "Codex Session:";
|
|
224
225
|
private metaStore: CodexSessionMetaStore;
|
|
226
|
+
private modelOverride: string | undefined;
|
|
225
227
|
|
|
226
|
-
constructor(metaStore: CodexSessionMetaStore) {
|
|
228
|
+
constructor(metaStore: CodexSessionMetaStore, modelOverride?: string) {
|
|
227
229
|
this.metaStore = metaStore;
|
|
230
|
+
this.modelOverride = modelOverride;
|
|
228
231
|
}
|
|
229
232
|
|
|
230
233
|
// createSession: 生成 sessionId,记录 cwd,不创建 Codex 线程(延迟到首次 prompt)
|
|
@@ -251,7 +254,7 @@ class CodexAdapter implements ToolAdapter {
|
|
|
251
254
|
? [...CODEX_BASE_ARGS, "-C", cwd, "-"]
|
|
252
255
|
: [...CODEX_BASE_ARGS, "resume", threadId, "-"];
|
|
253
256
|
|
|
254
|
-
const proc = spawnCodex(args, cwd, userText);
|
|
257
|
+
const proc = spawnCodex(args, cwd, userText, this.modelOverride);
|
|
255
258
|
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
256
259
|
|
|
257
260
|
// 关键:spawn 用了 shell:true,proc.pid 指向的是壳进程(cmd.exe / sh)。
|
|
@@ -304,6 +307,8 @@ class CodexAdapter implements ToolAdapter {
|
|
|
304
307
|
|
|
305
308
|
export interface CreateCodexAdapterOptions {
|
|
306
309
|
metaStore?: CodexSessionMetaStore;
|
|
310
|
+
/** per-session 模型覆盖(/model 命令);传了就用,不传走全局 codex.model */
|
|
311
|
+
model?: string;
|
|
307
312
|
}
|
|
308
313
|
|
|
309
314
|
export function createCodexAdapter(
|
|
@@ -311,5 +316,6 @@ export function createCodexAdapter(
|
|
|
311
316
|
): ToolAdapter {
|
|
312
317
|
return new CodexAdapter(
|
|
313
318
|
options.metaStore ?? defaultCodexSessionMetaStore,
|
|
319
|
+
options.model,
|
|
314
320
|
);
|
|
315
321
|
}
|
|
@@ -260,8 +260,18 @@ function spawnAgent(
|
|
|
260
260
|
extraArgs: string[],
|
|
261
261
|
cwd?: string,
|
|
262
262
|
stdinText?: string,
|
|
263
|
+
modelOverride?: string,
|
|
263
264
|
): ChildProcess {
|
|
264
|
-
|
|
265
|
+
let allArgs = [...CURSOR_AGENT_ARGS, ...extraArgs];
|
|
266
|
+
if (modelOverride) {
|
|
267
|
+
// 替换全局 --model 为 per-session override
|
|
268
|
+
const modelIdx = allArgs.findIndex((a, i) => a === "--model" && i + 1 < allArgs.length);
|
|
269
|
+
if (modelIdx >= 0) {
|
|
270
|
+
allArgs[modelIdx + 1] = modelOverride;
|
|
271
|
+
} else {
|
|
272
|
+
allArgs.push("--model", modelOverride);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
265
275
|
const proc = spawn(CURSOR_AGENT_COMMAND, allArgs, {
|
|
266
276
|
cwd,
|
|
267
277
|
stdio: [stdinText !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
|
|
@@ -324,13 +334,15 @@ class CursorAdapter implements ToolAdapter {
|
|
|
324
334
|
readonly sessionDescPrefix = "Cursor Session:";
|
|
325
335
|
private activeProcs = new Set<ChildProcess>();
|
|
326
336
|
private metaStore: CursorSessionMetaStore;
|
|
337
|
+
private modelOverride: string | undefined;
|
|
327
338
|
|
|
328
|
-
constructor(metaStore: CursorSessionMetaStore) {
|
|
339
|
+
constructor(metaStore: CursorSessionMetaStore, modelOverride?: string) {
|
|
329
340
|
this.metaStore = metaStore;
|
|
341
|
+
this.modelOverride = modelOverride;
|
|
330
342
|
}
|
|
331
343
|
|
|
332
344
|
async createSession(cwd: string): Promise<CreateSessionResult> {
|
|
333
|
-
const proc = spawnAgent(["ok"], cwd);
|
|
345
|
+
const proc = spawnAgent(["ok"], cwd, undefined, this.modelOverride);
|
|
334
346
|
this.activeProcs.add(proc);
|
|
335
347
|
|
|
336
348
|
for await (const msg of readJsonLines(proc, undefined, "createSession")) {
|
|
@@ -358,7 +370,7 @@ class CursorAdapter implements ToolAdapter {
|
|
|
358
370
|
options?: ToolPromptOptions,
|
|
359
371
|
): AsyncIterable<UnifiedStreamMessage> {
|
|
360
372
|
console.log(`[Cursor debug] prompt start: sessionId=${sessionId}, cwd=${cwd}, userTextLen=${userText.length}`);
|
|
361
|
-
const proc = spawnAgent(["--resume", sessionId], cwd, userText);
|
|
373
|
+
const proc = spawnAgent(["--resume", sessionId], cwd, userText, this.modelOverride);
|
|
362
374
|
this.activeProcs.add(proc);
|
|
363
375
|
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
364
376
|
|
|
@@ -414,10 +426,12 @@ class CursorAdapter implements ToolAdapter {
|
|
|
414
426
|
export interface CreateCursorAdapterOptions {
|
|
415
427
|
/** 注入自定义 meta 持久化 store(测试用);未提供时使用全局默认实例。 */
|
|
416
428
|
metaStore?: CursorSessionMetaStore;
|
|
429
|
+
/** per-session 模型覆盖(/model 命令);传了就用,不传走全局 cursor.model */
|
|
430
|
+
model?: string;
|
|
417
431
|
}
|
|
418
432
|
|
|
419
433
|
export function createCursorAdapter(
|
|
420
434
|
options: CreateCursorAdapterOptions = {},
|
|
421
435
|
): ToolAdapter {
|
|
422
|
-
return new CursorAdapter(options.metaStore ?? defaultCursorSessionMetaStore);
|
|
436
|
+
return new CursorAdapter(options.metaStore ?? defaultCursorSessionMetaStore, options.model);
|
|
423
437
|
}
|
package/src/cards.ts
CHANGED
|
@@ -408,58 +408,41 @@ export function buildStatusCard(statusText: string, template = "blue"): string {
|
|
|
408
408
|
});
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
export interface ModelSwitchOption {
|
|
412
|
-
label: string;
|
|
413
|
-
model: string;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
411
|
/** 模型切换卡片(/model 命令,飞书专用,含切换按钮) */
|
|
417
412
|
export function buildModelCard(
|
|
418
413
|
currentModel: string,
|
|
419
|
-
|
|
414
|
+
models: string[],
|
|
415
|
+
tool?: string,
|
|
420
416
|
): string {
|
|
417
|
+
const toolLabel = tool ? ` (${tool})` : "";
|
|
421
418
|
const currentLine = currentModel
|
|
422
419
|
? `**当前模型:** \`${currentModel}\``
|
|
423
420
|
: "**当前模型:** 未指定";
|
|
424
|
-
const hasPro = available.some(o => o.label === "pro");
|
|
425
|
-
const hasFlash = available.some(o => o.label === "flash");
|
|
426
421
|
|
|
427
422
|
const lines: string[] = [currentLine];
|
|
428
|
-
if (
|
|
423
|
+
if (models.length > 0) {
|
|
429
424
|
lines.push("", "**可切换模型:**");
|
|
430
|
-
for (const
|
|
431
|
-
lines.push(`-
|
|
425
|
+
for (const m of models) {
|
|
426
|
+
lines.push(`- \`${m}\``);
|
|
432
427
|
}
|
|
428
|
+
lines.push("", "点击按钮切换模型,或输入 `/model clear` 恢复默认");
|
|
433
429
|
} else {
|
|
434
|
-
lines.push("", "
|
|
430
|
+
lines.push("", "没有可切换的模型。请在 config.json 中配置模型字段。");
|
|
435
431
|
}
|
|
436
432
|
|
|
437
|
-
const buttons:
|
|
438
|
-
|
|
433
|
+
const buttons: ButtonDef[] = [];
|
|
434
|
+
for (const m of models.slice(0, 20)) {
|
|
435
|
+
const shortName = m.includes("/") ? m.slice(m.lastIndexOf("/") + 1) : m;
|
|
439
436
|
buttons.push({
|
|
440
|
-
text:
|
|
441
|
-
value: JSON.stringify({
|
|
437
|
+
text: `/model ${shortName}`,
|
|
438
|
+
value: JSON.stringify({ cmd: `/model ${m}` }),
|
|
442
439
|
type: "primary",
|
|
443
440
|
});
|
|
444
441
|
}
|
|
445
|
-
if (hasFlash) {
|
|
446
|
-
buttons.push({
|
|
447
|
-
text: "/model flash",
|
|
448
|
-
value: JSON.stringify({ action: "model_flash" }),
|
|
449
|
-
type: "primary",
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
if (!hasPro && !hasFlash) {
|
|
453
|
-
buttons.push({
|
|
454
|
-
text: "/model clear",
|
|
455
|
-
value: JSON.stringify({ action: "model_clear" }),
|
|
456
|
-
type: "default",
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
442
|
|
|
460
443
|
return JSON.stringify({
|
|
461
444
|
config: { wide_screen_mode: true },
|
|
462
|
-
header: { template: "blue", title: { content:
|
|
445
|
+
header: { template: "blue", title: { content: `模型切换${toolLabel}`, tag: "plain_text" } },
|
|
463
446
|
elements: [
|
|
464
447
|
{ tag: "div", text: { tag: "lark_md", content: lines.join("\n") } },
|
|
465
448
|
{ tag: "hr" },
|
package/src/config.ts
CHANGED
|
@@ -120,6 +120,25 @@ export interface AppConfig {
|
|
|
120
120
|
export type AgentTool = "claude" | "cursor" | "codex";
|
|
121
121
|
export const AGENT_TOOLS: AgentTool[] = ["claude", "cursor", "codex"];
|
|
122
122
|
|
|
123
|
+
/** 获取指定 agent 配置中所有模型相关的值(最多 100 个,去重) */
|
|
124
|
+
export function getAllModelsForTool(tool: AgentTool, cfg: AppConfig = config): string[] {
|
|
125
|
+
const seen = new Set<string>();
|
|
126
|
+
const collect = (v: unknown) => {
|
|
127
|
+
if (typeof v === "string" && v.trim()) seen.add(v.trim());
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (tool === "claude") {
|
|
131
|
+
collect(cfg.claude.model);
|
|
132
|
+
collect(cfg.claude.subagentModel);
|
|
133
|
+
} else if (tool === "cursor") {
|
|
134
|
+
collect(cfg.cursor.model);
|
|
135
|
+
} else if (tool === "codex") {
|
|
136
|
+
collect(cfg.codex.model);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return Array.from(seen).slice(0, 100);
|
|
140
|
+
}
|
|
141
|
+
|
|
123
142
|
const CONFIG_FILE = join(USER_DATA_DIR, "config.json");
|
|
124
143
|
const CONFIG_SAMPLE_FILE = join(PROJECT_ROOT, "config.sample.json");
|
|
125
144
|
|
package/src/feishu-api.ts
CHANGED
|
@@ -532,15 +532,92 @@ export function formatDelayNotice(createTimeMs: number, messageText?: string, no
|
|
|
532
532
|
return `> ⚠️ 延迟送达提醒:此消息于 ${sendTimeStr} 发送,因服务离线,延迟约 ${delayStr}后送达${contentLine}`;
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
/**
|
|
536
|
+
* 检测文本中的 markdown 表格,用代码块包裹。
|
|
537
|
+
*
|
|
538
|
+
* 飞书 markdown 渲染对表格支持不稳定(列对齐易错乱),用代码块包裹可保证
|
|
539
|
+
* 等宽字体显示、列对齐正确。只处理不在已有代码块内的表格。
|
|
540
|
+
*
|
|
541
|
+
* 检测规则:至少两行连续的 "|col|col|" 模式,其中必须包含一行分隔行
|
|
542
|
+
* (如 |---|----|),避免将单行含 | 的普通文本误判为表格。
|
|
543
|
+
*/
|
|
544
|
+
function wrapMarkdownTables(text: string): string {
|
|
545
|
+
const lines = text.split("\n");
|
|
546
|
+
const result: string[] = [];
|
|
547
|
+
let inCodeBlock = false;
|
|
548
|
+
let tableRows: string[] = [];
|
|
549
|
+
let foundSeparator = false;
|
|
550
|
+
|
|
551
|
+
const isTableRow = (line: string): boolean =>
|
|
552
|
+
/^\s*\|.+\|/.test(line);
|
|
553
|
+
|
|
554
|
+
const isSeparatorRow = (line: string): boolean =>
|
|
555
|
+
/^\s*\|[\s\-:]+\|/.test(line) && /-/.test(line);
|
|
556
|
+
|
|
557
|
+
const flushTable = (): void => {
|
|
558
|
+
if (foundSeparator && tableRows.length >= 2) {
|
|
559
|
+
result.push("```");
|
|
560
|
+
result.push(...tableRows);
|
|
561
|
+
result.push("```");
|
|
562
|
+
} else {
|
|
563
|
+
result.push(...tableRows);
|
|
564
|
+
}
|
|
565
|
+
tableRows = [];
|
|
566
|
+
foundSeparator = false;
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
for (const line of lines) {
|
|
570
|
+
const trimmed = line.trim();
|
|
571
|
+
|
|
572
|
+
if (trimmed.startsWith("```")) {
|
|
573
|
+
flushTable();
|
|
574
|
+
inCodeBlock = !inCodeBlock;
|
|
575
|
+
result.push(line);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (inCodeBlock) {
|
|
580
|
+
result.push(line);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (!foundSeparator) {
|
|
585
|
+
if (isTableRow(line)) {
|
|
586
|
+
tableRows.push(line);
|
|
587
|
+
} else if (tableRows.length > 0 && isSeparatorRow(line)) {
|
|
588
|
+
tableRows.push(line);
|
|
589
|
+
foundSeparator = true;
|
|
590
|
+
} else {
|
|
591
|
+
if (tableRows.length > 0) {
|
|
592
|
+
result.push(...tableRows);
|
|
593
|
+
tableRows = [];
|
|
594
|
+
}
|
|
595
|
+
result.push(line);
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
if (isTableRow(line)) {
|
|
599
|
+
tableRows.push(line);
|
|
600
|
+
} else {
|
|
601
|
+
flushTable();
|
|
602
|
+
result.push(line);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
flushTable();
|
|
608
|
+
return result.join("\n");
|
|
609
|
+
}
|
|
610
|
+
|
|
535
611
|
export async function sendTextReply(
|
|
536
612
|
token: string,
|
|
537
613
|
chatId: string,
|
|
538
614
|
text: string
|
|
539
615
|
): Promise<boolean> {
|
|
540
616
|
const safeText = applyPrivacy(text);
|
|
617
|
+
const wrappedText = wrapMarkdownTables(safeText);
|
|
541
618
|
const card = JSON.stringify({
|
|
542
619
|
config: { wide_screen_mode: true },
|
|
543
|
-
elements: [{ tag: "markdown", content:
|
|
620
|
+
elements: [{ tag: "markdown", content: wrappedText }],
|
|
544
621
|
});
|
|
545
622
|
try {
|
|
546
623
|
const resp = await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
|
|
@@ -915,3 +992,51 @@ export async function updateCardMessage(token: string, messageId: string, conten
|
|
|
915
992
|
}
|
|
916
993
|
return true;
|
|
917
994
|
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* 发送飞书 post 类型消息(非卡片富文本消息)。
|
|
998
|
+
*
|
|
999
|
+
* post 消息支持 table、text、a、at、img 等富文本元素,
|
|
1000
|
+
* 与 interactive 卡片消息不同,post 直接在消息流中渲染。
|
|
1001
|
+
*
|
|
1002
|
+
* @param postContent 飞书 post content 格式的二维数组,每个元素是一个 paragraph
|
|
1003
|
+
*/
|
|
1004
|
+
export async function sendPostMessage(
|
|
1005
|
+
token: string,
|
|
1006
|
+
chatId: string,
|
|
1007
|
+
title: string,
|
|
1008
|
+
postContent: unknown[][],
|
|
1009
|
+
): Promise<boolean> {
|
|
1010
|
+
const content = JSON.stringify({
|
|
1011
|
+
post: {
|
|
1012
|
+
zh_cn: {
|
|
1013
|
+
title,
|
|
1014
|
+
content: postContent,
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
});
|
|
1018
|
+
try {
|
|
1019
|
+
const resp = await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
|
|
1020
|
+
method: "POST",
|
|
1021
|
+
headers: {
|
|
1022
|
+
Authorization: `Bearer ${token}`,
|
|
1023
|
+
"Content-Type": "application/json",
|
|
1024
|
+
},
|
|
1025
|
+
body: JSON.stringify({
|
|
1026
|
+
receive_id: chatId,
|
|
1027
|
+
msg_type: "post",
|
|
1028
|
+
content,
|
|
1029
|
+
}),
|
|
1030
|
+
});
|
|
1031
|
+
const data = (await resp.json().catch(() => ({}))) as { code: number; msg?: string; data?: { message_id?: string } };
|
|
1032
|
+
if (data.code !== 0) {
|
|
1033
|
+
console.error(`[${ts()}] [SEND] post FAIL: chatId=${chatId} code=${data.code} msg="${data.msg ?? ""}"`);
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
console.log(`[${ts()}] [SEND] post OK: chatId=${chatId} msgId=${data.data?.message_id ?? "N/A"}`);
|
|
1037
|
+
return true;
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
console.error(`[${ts()}] [SEND] post FAIL: chatId=${chatId} ${(err as Error).message}`);
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
package/src/feishu-platform.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface FeishuPlatform {
|
|
|
19
19
|
sendTextReply: typeof realApi.sendTextReply;
|
|
20
20
|
sendCardReply: typeof realApi.sendCardReply;
|
|
21
21
|
sendRawCard: typeof realApi.sendRawCard;
|
|
22
|
+
sendPostMessage: typeof realApi.sendPostMessage;
|
|
22
23
|
sendImageReply: typeof realApi.sendImageReply;
|
|
23
24
|
sendFileReply: typeof realApi.sendFileReply;
|
|
24
25
|
addReaction: typeof realApi.addReaction;
|
|
@@ -134,6 +135,10 @@ export function formatDelayNotice(...args: Parameters<typeof realApi.formatDelay
|
|
|
134
135
|
return _impl.formatDelayNotice(...args);
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
export function sendPostMessage(...args: Parameters<typeof realApi.sendPostMessage>): ReturnType<typeof realApi.sendPostMessage> {
|
|
139
|
+
return _impl.sendPostMessage(...args);
|
|
140
|
+
}
|
|
141
|
+
|
|
137
142
|
export function sendRestartCard(...args: Parameters<typeof realApi.sendRestartCard>): ReturnType<typeof realApi.sendRestartCard> {
|
|
138
143
|
return _impl.sendRestartCard(...args);
|
|
139
144
|
}
|
package/src/index.ts
CHANGED
|
@@ -285,12 +285,14 @@ function parseCardAction(data: unknown): CardActionResult | null {
|
|
|
285
285
|
}
|
|
286
286
|
if (!cmd) return null;
|
|
287
287
|
|
|
288
|
-
const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions", newh: "/newh"
|
|
288
|
+
const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions", newh: "/newh" };
|
|
289
289
|
let text = CMD_MAP[cmd] ?? "";
|
|
290
290
|
if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
|
|
291
291
|
const path = (action.value as Record<string, string>).path;
|
|
292
292
|
if (path) text = `/cd ${path}`;
|
|
293
293
|
}
|
|
294
|
+
// cmd 本身就是以 / 开头的完整指令时,直接使用(如 /model <name> 动态按钮)
|
|
295
|
+
if (!text && cmd.startsWith("/")) text = cmd;
|
|
294
296
|
if (!text) return null;
|
|
295
297
|
|
|
296
298
|
const chatId =
|
package/src/orchestrator.ts
CHANGED
|
@@ -17,7 +17,9 @@ import {
|
|
|
17
17
|
GIT_TIMEOUT_MS,
|
|
18
18
|
PROJECT_ROOT,
|
|
19
19
|
anthropicConfigDisplay,
|
|
20
|
+
config,
|
|
20
21
|
fileLog,
|
|
22
|
+
getAllModelsForTool,
|
|
21
23
|
getDefaultCwd,
|
|
22
24
|
setDefaultCwd,
|
|
23
25
|
getRecentDirs,
|
|
@@ -26,6 +28,7 @@ import {
|
|
|
26
28
|
sessionPrefixForTool,
|
|
27
29
|
toolDisplayName,
|
|
28
30
|
ts,
|
|
31
|
+
type AgentTool,
|
|
29
32
|
} from "./config.ts";
|
|
30
33
|
import {
|
|
31
34
|
buildHelpCard,
|
|
@@ -54,6 +57,7 @@ import {
|
|
|
54
57
|
switchChatBinding,
|
|
55
58
|
recordSessionRegistry,
|
|
56
59
|
getAdapterForTool,
|
|
60
|
+
getEffectiveModelForTool,
|
|
57
61
|
stopSession,
|
|
58
62
|
loadSessionRegistryForBinding,
|
|
59
63
|
removeSessionRegistryRecord,
|
|
@@ -69,6 +73,7 @@ import {
|
|
|
69
73
|
enqueueMessage,
|
|
70
74
|
cancelQueuedMessage,
|
|
71
75
|
} from "./session-chat-binding.ts";
|
|
76
|
+
import { getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
|
|
72
77
|
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
73
78
|
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
74
79
|
|
|
@@ -85,6 +90,21 @@ export function sessionChatName(left: string, cwd: string): string {
|
|
|
85
90
|
return `${left}-${cwdDisplayName(cwd)}`;
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
/** 模型模糊匹配:精确匹配优先,否则找子串匹配(模型名越短越优先) */
|
|
94
|
+
function findModelMatch(input: string, models: string[]): string | null {
|
|
95
|
+
if (models.length === 0) return null;
|
|
96
|
+
const inputLower = input.toLowerCase();
|
|
97
|
+
// 1) 精确匹配(忽略大小写)
|
|
98
|
+
for (const m of models) {
|
|
99
|
+
if (m.toLowerCase() === inputLower) return m;
|
|
100
|
+
}
|
|
101
|
+
// 2) 子串匹配:模型全名包含输入,按模型名长度升序(越短越优先)
|
|
102
|
+
const candidates = models
|
|
103
|
+
.filter(m => m.toLowerCase().includes(inputLower))
|
|
104
|
+
.sort((a, b) => a.length - b.length);
|
|
105
|
+
return candidates[0] ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
function isUntitledSessionChatName(name: string): boolean {
|
|
89
109
|
return name === "新会话" || name.startsWith("新会话-");
|
|
90
110
|
}
|
|
@@ -250,48 +270,28 @@ export async function handleCommand(
|
|
|
250
270
|
if (textLower === "/model") {
|
|
251
271
|
logTrace(tid, "BRANCH", { cmd: "/model" });
|
|
252
272
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
: platform.sendCard(chatId, "模型切换", msg, "red")
|
|
260
|
-
).catch(() => {});
|
|
261
|
-
logTrace(tid, "DONE", { outcome: "model_unsupported" });
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const findModel = (keyword: string): string | null => {
|
|
266
|
-
for (const name of [CLAUDE_MODEL.trim(), CLAUDE_SUBAGENT_MODEL.trim()]) {
|
|
267
|
-
if (!name) continue;
|
|
268
|
-
const nl = name.toLowerCase();
|
|
269
|
-
if (nl.includes("deepseek") && nl.includes(keyword)) return name;
|
|
270
|
-
}
|
|
271
|
-
return null;
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
const available: { label: string; model: string }[] = [];
|
|
275
|
-
const pm = findModel("pro");
|
|
276
|
-
if (pm) available.push({ label: "pro", model: pm });
|
|
277
|
-
const fm = findModel("flash");
|
|
278
|
-
if (fm) available.push({ label: "flash", model: fm });
|
|
273
|
+
const defaultTool = resolveDefaultAgentTool();
|
|
274
|
+
const models = getAllModelsForTool(defaultTool);
|
|
275
|
+
let currentModel = "";
|
|
276
|
+
if (defaultTool === "cursor") currentModel = config.cursor.model;
|
|
277
|
+
else if (defaultTool === "codex") currentModel = config.codex.model;
|
|
278
|
+
else currentModel = CLAUDE_MODEL;
|
|
279
279
|
|
|
280
280
|
if (platform.kind === "wechat") {
|
|
281
|
-
const lines = [
|
|
282
|
-
if (
|
|
281
|
+
const lines = [currentModel ? `当前模型 (${defaultTool}): ${currentModel}` : `当前模型 (${defaultTool}): 未指定`];
|
|
282
|
+
if (models.length > 0) {
|
|
283
283
|
lines.push("", "可切换模型:");
|
|
284
|
-
for (const
|
|
285
|
-
lines.push("", "
|
|
284
|
+
for (const m of models) lines.push(` ${m}`);
|
|
285
|
+
lines.push("", "在会话中输入 /model <模型名> 切换模型");
|
|
286
286
|
} else {
|
|
287
|
-
lines.push("", "没有可切换的模型。请在 config.json
|
|
287
|
+
lines.push("", "没有可切换的模型。请在 config.json 中配置模型字段。");
|
|
288
288
|
}
|
|
289
289
|
await platform.sendText(chatId, lines.join("\n")).catch(() => {});
|
|
290
290
|
} else {
|
|
291
|
-
const card = buildModelCard(
|
|
291
|
+
const card = buildModelCard(currentModel, models, defaultTool);
|
|
292
292
|
await platform.sendRawCard(chatId, card);
|
|
293
293
|
}
|
|
294
|
-
logTrace(tid, "DONE", { outcome: "model_query" });
|
|
294
|
+
logTrace(tid, "DONE", { outcome: "model_query", defaultTool });
|
|
295
295
|
return;
|
|
296
296
|
}
|
|
297
297
|
|
|
@@ -391,6 +391,7 @@ export async function handleCommand(
|
|
|
391
391
|
`**工作目录:** \`${cwd}\`\n\n` +
|
|
392
392
|
`直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
|
|
393
393
|
`发送 **/cd** 切换新建会话的默认目录。\n` +
|
|
394
|
+
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
394
395
|
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
395
396
|
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
396
397
|
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。`,
|
|
@@ -476,6 +477,7 @@ export async function handleCommand(
|
|
|
476
477
|
`**工作目录:** \`${cwd}\`\n\n` +
|
|
477
478
|
`直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
|
|
478
479
|
`发送 **/cd** 切换新建会话的默认目录。\n` +
|
|
480
|
+
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
479
481
|
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
480
482
|
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
481
483
|
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。`,
|
|
@@ -660,6 +662,43 @@ export async function handleCommand(
|
|
|
660
662
|
return;
|
|
661
663
|
}
|
|
662
664
|
|
|
665
|
+
if (textLower === "/test") {
|
|
666
|
+
logTrace(tid, "BRANCH", { cmd: "/test" });
|
|
667
|
+
const tableHeaders = ["名称", "版本", "状态"];
|
|
668
|
+
const tableRows = [
|
|
669
|
+
["ChatCCC", "0.2.96", "运行中"],
|
|
670
|
+
["Claude SDK", "0.50.0", "已连接"],
|
|
671
|
+
["Feishu API", "v1", "正常"],
|
|
672
|
+
];
|
|
673
|
+
const mdTable = [
|
|
674
|
+
`| ${tableHeaders.join(" | ")} |`,
|
|
675
|
+
`| ${tableHeaders.map(() => "---").join(" | ")} |`,
|
|
676
|
+
...tableRows.map((row) => `| ${row.join(" | ")} |`),
|
|
677
|
+
].join("\n");
|
|
678
|
+
|
|
679
|
+
if (platform.kind === "feishu") {
|
|
680
|
+
try {
|
|
681
|
+
const token = await getTenantAccessToken();
|
|
682
|
+
const postContent: unknown[][] = [
|
|
683
|
+
// 先尝试富文本表格
|
|
684
|
+
[{ tag: "table", cells: [tableHeaders, ...tableRows] }],
|
|
685
|
+
// 再用代码块包起来
|
|
686
|
+
[{ tag: "text", text: `\n表格(代码块格式):\n\`\`\`\n${mdTable}\n\`\`\`` }],
|
|
687
|
+
];
|
|
688
|
+
await sendPostMessage(token, chatId, "测试表格", postContent);
|
|
689
|
+
} catch (err) {
|
|
690
|
+
console.error(`[${ts()}] [TEST] post message failed: ${(err as Error).message}`);
|
|
691
|
+
// Fallback to markdown card
|
|
692
|
+
await platform.sendText(chatId, `表格(代码块格式):\n\`\`\`\n${mdTable}\n\`\`\``).catch(() => {});
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
// WeChat / other platforms: just send code block
|
|
696
|
+
await platform.sendText(chatId, `表格(代码块格式):\n\`\`\`\n${mdTable}\n\`\`\``).catch(() => {});
|
|
697
|
+
}
|
|
698
|
+
logTrace(tid, "DONE", { outcome: "test" });
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
663
702
|
if (textLower === "/status") {
|
|
664
703
|
logTrace(tid, "BRANCH", { cmd: "/status" });
|
|
665
704
|
const status = await getSessionStatus(chatId);
|
|
@@ -799,7 +838,8 @@ export async function handleCommand(
|
|
|
799
838
|
`**Session ID:** ${newSessionId}\n` +
|
|
800
839
|
`**工作目录:** \`${cwd}\`(沿用当前会话目录)\n\n` +
|
|
801
840
|
`直接在这里发消息即可继续对话。\n` +
|
|
802
|
-
`发送 **/cd**
|
|
841
|
+
`发送 **/cd** 可切换新建会话的默认目录。\n` +
|
|
842
|
+
`发送 **/model** 查看或切换当前会话的模型。`,
|
|
803
843
|
"green",
|
|
804
844
|
);
|
|
805
845
|
|
|
@@ -959,7 +999,8 @@ export async function handleCommand(
|
|
|
959
999
|
`**序号:** ${index + 1}\n` +
|
|
960
1000
|
`**Session ID:** ${target.sessionId}\n` +
|
|
961
1001
|
`**工作目录:** \`${cwd2}\`\n\n` +
|
|
962
|
-
|
|
1002
|
+
`直接在这里发消息即可继续对话。\n` +
|
|
1003
|
+
`发送 **/model** 查看或切换当前会话的模型。${busyNote}`,
|
|
963
1004
|
"green",
|
|
964
1005
|
);
|
|
965
1006
|
|
|
@@ -972,61 +1013,52 @@ export async function handleCommand(
|
|
|
972
1013
|
return;
|
|
973
1014
|
}
|
|
974
1015
|
|
|
975
|
-
// /model
|
|
976
|
-
if (textLower === "/model
|
|
977
|
-
|
|
978
|
-
|
|
1016
|
+
// /model clear — 清除当前 session 的模型覆盖
|
|
1017
|
+
if (textLower === "/model clear") {
|
|
1018
|
+
logTrace(tid, "BRANCH", { cmd: "/model clear", sessionId });
|
|
1019
|
+
clearSessionModelOverride(sessionId);
|
|
1020
|
+
const defaultModel = getEffectiveModelForTool(descriptionTool);
|
|
1021
|
+
const toolLabel = toolDisplayName(descriptionTool);
|
|
1022
|
+
const msg = `已清除当前 ${toolLabel} 会话的模型覆盖,恢复使用: \`${defaultModel || "(未指定)"}\``;
|
|
1023
|
+
await (platform.kind === "wechat"
|
|
1024
|
+
? platform.sendText(chatId, msg)
|
|
1025
|
+
: platform.sendCard(chatId, "模型切换", msg, "green")
|
|
1026
|
+
).catch(() => {});
|
|
1027
|
+
logTrace(tid, "DONE", { outcome: "model_cleared", sessionId, tool: descriptionTool });
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
979
1030
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
: platform.sendCard(chatId, "模型切换", msg, "red")
|
|
986
|
-
).catch(() => {});
|
|
987
|
-
logTrace(tid, "DONE", { outcome: "model_wrong_tool", tool: descriptionTool });
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
1031
|
+
// /model <name> — 切换当前 session 的模型(支持所有 agent,模糊匹配)
|
|
1032
|
+
if (textLower.startsWith("/model ")) {
|
|
1033
|
+
const modelArg = text.slice(7).trim();
|
|
1034
|
+
if (!modelArg) return; // 纯 "/model " 不处理,交给上面的 /model 分支
|
|
1035
|
+
logTrace(tid, "BRANCH", { cmd: "/model", arg: modelArg, sessionId, tool: descriptionTool });
|
|
990
1036
|
|
|
991
|
-
const
|
|
992
|
-
|
|
993
|
-
if (!name) continue;
|
|
994
|
-
const nl = name.toLowerCase();
|
|
995
|
-
if (nl.includes("deepseek") && nl.includes(keyword)) return name;
|
|
996
|
-
}
|
|
997
|
-
return null;
|
|
998
|
-
};
|
|
1037
|
+
const models = getAllModelsForTool(descriptionTool as AgentTool);
|
|
1038
|
+
const toolLabel = toolDisplayName(descriptionTool);
|
|
999
1039
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const restored = CLAUDE_MODEL.trim() || "(未指定)";
|
|
1003
|
-
const msg = `已清除当前会话的模型覆盖,恢复使用: \`${restored}\``;
|
|
1004
|
-
await (platform.kind === "wechat"
|
|
1005
|
-
? platform.sendText(chatId, msg)
|
|
1006
|
-
: platform.sendCard(chatId, "模型切换", msg, "green")
|
|
1007
|
-
).catch(() => {});
|
|
1008
|
-
logTrace(tid, "DONE", { outcome: "model_cleared", sessionId });
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1040
|
+
// 查找目标模型:精确匹配优先,否则子串匹配(模型名越短越优先)
|
|
1041
|
+
const target = findModelMatch(modelArg, models);
|
|
1011
1042
|
|
|
1012
|
-
const target = findModel(modelArg);
|
|
1013
1043
|
if (!target) {
|
|
1014
|
-
const msg =
|
|
1044
|
+
const msg = models.length > 0
|
|
1045
|
+
? `未找到匹配 "${modelArg}" 的模型。当前 ${toolLabel} 可选模型:\n${models.map(m => ` \`${m}\``).join("\n")}`
|
|
1046
|
+
: `当前 ${toolLabel} 没有可切换的模型。请在 config.json 中配置模型字段。`;
|
|
1015
1047
|
await (platform.kind === "wechat"
|
|
1016
1048
|
? platform.sendText(chatId, msg)
|
|
1017
1049
|
: platform.sendCard(chatId, "模型切换", msg, "red")
|
|
1018
1050
|
).catch(() => {});
|
|
1019
|
-
logTrace(tid, "DONE", { outcome: "model_not_found", arg: modelArg });
|
|
1051
|
+
logTrace(tid, "DONE", { outcome: "model_not_found", arg: modelArg, tool: descriptionTool });
|
|
1020
1052
|
return;
|
|
1021
1053
|
}
|
|
1022
1054
|
|
|
1023
1055
|
setSessionModelOverride(sessionId, target);
|
|
1024
|
-
const msg =
|
|
1056
|
+
const msg = `已切换当前 ${toolLabel} 会话模型为: \`${target}\``;
|
|
1025
1057
|
await (platform.kind === "wechat"
|
|
1026
1058
|
? platform.sendText(chatId, msg)
|
|
1027
1059
|
: platform.sendCard(chatId, "模型切换", msg, "green")
|
|
1028
1060
|
).catch(() => {});
|
|
1029
|
-
logTrace(tid, "DONE", { outcome: "model_switched", arg: modelArg, target, sessionId });
|
|
1061
|
+
logTrace(tid, "DONE", { outcome: "model_switched", arg: modelArg, target, sessionId, tool: descriptionTool });
|
|
1030
1062
|
return;
|
|
1031
1063
|
}
|
|
1032
1064
|
|
package/src/session.ts
CHANGED
|
@@ -315,7 +315,7 @@ const adapterCache = new Map<string, ToolAdapter>();
|
|
|
315
315
|
// Per-session 模型覆盖(/model 命令设置,不持久化)
|
|
316
316
|
const sessionModelOverrides = new Map<string, string>();
|
|
317
317
|
|
|
318
|
-
/** 返回 session 的生效模型:优先 per-session
|
|
318
|
+
/** 返回 session 的生效模型:优先 per-session 覆盖,其次全局配置(Claude) */
|
|
319
319
|
function getModelForSession(sessionId?: string): string {
|
|
320
320
|
if (sessionId) {
|
|
321
321
|
const override = sessionModelOverrides.get(sessionId);
|
|
@@ -324,7 +324,18 @@ function getModelForSession(sessionId?: string): string {
|
|
|
324
324
|
return CLAUDE_MODEL;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
/**
|
|
327
|
+
/** 返回指定 tool 的生效模型:优先 per-session 覆盖,其次 tool 默认配置 */
|
|
328
|
+
export function getEffectiveModelForTool(tool: string, sessionId?: string): string {
|
|
329
|
+
if (sessionId) {
|
|
330
|
+
const override = sessionModelOverrides.get(sessionId);
|
|
331
|
+
if (override) return override;
|
|
332
|
+
}
|
|
333
|
+
if (tool === "cursor") return config.cursor.model;
|
|
334
|
+
if (tool === "codex") return config.codex.model;
|
|
335
|
+
return CLAUDE_MODEL;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** 为指定 session 设置模型覆盖(/model <name>) */
|
|
328
339
|
export function setSessionModelOverride(sessionId: string, model: string): void {
|
|
329
340
|
sessionModelOverrides.set(sessionId, model);
|
|
330
341
|
adapterCache.clear();
|
|
@@ -337,16 +348,16 @@ export function clearSessionModelOverride(sessionId: string): void {
|
|
|
337
348
|
}
|
|
338
349
|
|
|
339
350
|
export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter {
|
|
340
|
-
const effectiveModel = tool
|
|
341
|
-
const cacheKey =
|
|
351
|
+
const effectiveModel = getEffectiveModelForTool(tool, sessionId);
|
|
352
|
+
const cacheKey = effectiveModel ? `${tool}:${effectiveModel}` : tool;
|
|
342
353
|
const cached = adapterCache.get(cacheKey);
|
|
343
354
|
if (cached) return cached;
|
|
344
355
|
|
|
345
356
|
let adapter: ToolAdapter;
|
|
346
357
|
if (tool === "cursor") {
|
|
347
|
-
adapter = createCursorAdapter();
|
|
358
|
+
adapter = createCursorAdapter({ model: effectiveModel || undefined });
|
|
348
359
|
} else if (tool === "codex") {
|
|
349
|
-
adapter = createCodexAdapter();
|
|
360
|
+
adapter = createCodexAdapter({ model: effectiveModel || undefined });
|
|
350
361
|
} else {
|
|
351
362
|
adapter = createClaudeAdapter({
|
|
352
363
|
model: effectiveModel,
|
|
@@ -534,9 +545,13 @@ export function _resetSessionRegistryFileForTest(): void {
|
|
|
534
545
|
// accumulateBlockContent — 将 UnifiedBlock 累积到渲染状态(纯函数,可测试)
|
|
535
546
|
// ---------------------------------------------------------------------------
|
|
536
547
|
|
|
548
|
+
// 注意:以下字段命名含 "final",但实际语义是"本轮所有文本的累积",并非仅
|
|
549
|
+
// "最后一段回复"。历史原因得名——当时以为 finalReply 只包含最后一轮输出,实则
|
|
550
|
+
// 所有 text block(工具调用前、调用间、调用后)都累加在这里。
|
|
551
|
+
// accumulatedContent + finalReply 拼接后才是完整流式输出的全部内容。
|
|
537
552
|
export interface AccumulatorState {
|
|
538
553
|
accumulatedContent: string;
|
|
539
|
-
/**
|
|
554
|
+
/** 本轮所有 text block 的累加(流式文本 delta),包括工具调用前后的全部文本 */
|
|
540
555
|
finalText: string;
|
|
541
556
|
/**
|
|
542
557
|
* 适配器明确给出的"完整最终文本"(覆盖语义)。
|
|
@@ -1691,6 +1706,9 @@ export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
|
|
|
1691
1706
|
|
|
1692
1707
|
export function _setAdapterForToolForTest(tool: string, adapter: ToolAdapter): void {
|
|
1693
1708
|
adapterCache.set(tool, adapter);
|
|
1709
|
+
// 同时设置当前配置模型对应的 key(getAdapterForTool 会优先 lookup 含 model 的 key)
|
|
1710
|
+
const effective = getEffectiveModelForTool(tool);
|
|
1711
|
+
if (effective) adapterCache.set(`${tool}:${effective}`, adapter);
|
|
1694
1712
|
}
|
|
1695
1713
|
|
|
1696
1714
|
export function clearAdapterCache(): void {
|
package/src/sim-platform.ts
CHANGED
|
@@ -78,6 +78,12 @@ export const SimulatedPlatform: FeishuPlatform = {
|
|
|
78
78
|
return true;
|
|
79
79
|
},
|
|
80
80
|
|
|
81
|
+
async sendPostMessage(_token, chatId, title, postContent) {
|
|
82
|
+
console.log(`[${ts()}] [SIM:SEND] post → ${chatId}: title="${title}" paragraphs=${postContent.length}`);
|
|
83
|
+
simStore.sendReply(chatId, "post", `[${title}] ${postContent.length} paragraphs`);
|
|
84
|
+
return true;
|
|
85
|
+
},
|
|
86
|
+
|
|
81
87
|
// ---- 消息管理 ----
|
|
82
88
|
async addReaction(_token, _messageId, _emojiType) {
|
|
83
89
|
// 模拟模式不需要表情回应
|
package/src/sim-store.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface SimMessage {
|
|
|
30
30
|
id: string; // UUID
|
|
31
31
|
chatId: string;
|
|
32
32
|
senderId: string; // SimAccount.id
|
|
33
|
-
type: "text" | "card" | "image" | "file" | "raw_card" | "system";
|
|
33
|
+
type: "text" | "card" | "image" | "file" | "raw_card" | "post" | "system";
|
|
34
34
|
content: string; // 文本内容或 JSON 字符串
|
|
35
35
|
timestamp: number;
|
|
36
36
|
}
|
package/src/stream-state.ts
CHANGED
|
@@ -9,17 +9,20 @@ import { USER_DATA_DIR, ts } from "./config.ts";
|
|
|
9
9
|
|
|
10
10
|
export const STREAMS_DIR = join(USER_DATA_DIR, "state", "streams");
|
|
11
11
|
|
|
12
|
-
export interface StreamState {
|
|
13
|
-
sessionId: string;
|
|
14
|
-
status: "running" | "done" | "stopped" | "error";
|
|
15
|
-
accumulatedContent: string;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
export interface StreamState {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
status: "running" | "done" | "stopped" | "error";
|
|
15
|
+
accumulatedContent: string;
|
|
16
|
+
/** 本轮会话中 LLM 输出的全部文本内容(所有 text block 的累加)。
|
|
17
|
+
* 命名含 "final" 但实为"全部累积文本",并非仅"最终一段回复"。
|
|
18
|
+
* 参见 session.ts 的 AccumulatorState 注释。 */
|
|
19
|
+
finalReply: string;
|
|
20
|
+
/** The turn whose terminal text reply has already been delivered to IM. */
|
|
21
|
+
finalReplySentTurn?: number;
|
|
22
|
+
finalReplySentAt?: number;
|
|
23
|
+
chunkCount: number;
|
|
24
|
+
turnCount: number;
|
|
25
|
+
contextTokens: number;
|
|
23
26
|
updatedAt: number;
|
|
24
27
|
cwd: string;
|
|
25
28
|
tool: string;
|
|
@@ -74,7 +77,7 @@ export function _resetRenameForTest(): void {
|
|
|
74
77
|
* 真文件,这一帧可能被 reader 读到半截,但 readStreamState 的 try/catch 兜底
|
|
75
78
|
* 会让 loop 跳过这一帧,2 秒后下次 write 大概率成功——比写盘失败丢数据好。
|
|
76
79
|
*/
|
|
77
|
-
export async function writeStreamState(state: StreamState): Promise<void> {
|
|
80
|
+
export async function writeStreamState(state: StreamState): Promise<void> {
|
|
78
81
|
const filePath = getStreamStatePath(state.sessionId);
|
|
79
82
|
const payload = JSON.stringify(state, null, 2);
|
|
80
83
|
try {
|
|
@@ -98,26 +101,26 @@ export async function writeStreamState(state: StreamState): Promise<void> {
|
|
|
98
101
|
} catch (err) {
|
|
99
102
|
console.error(`[${ts()}] Failed to write stream-state for ${state.sessionId}: ${(err as Error).message}`);
|
|
100
103
|
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function isFinalReplySentForTurn(state: Pick<StreamState, "turnCount" | "finalReplySentTurn">): boolean {
|
|
104
|
-
return state.finalReplySentTurn === state.turnCount;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export async function markFinalReplySent(sessionId: string, turnCount: number, sentAt = Date.now()): Promise<void> {
|
|
108
|
-
const state = await readStreamState(sessionId);
|
|
109
|
-
if (!state) return;
|
|
110
|
-
if (state.turnCount !== turnCount) return;
|
|
111
|
-
if (state.status === "running") return;
|
|
112
|
-
|
|
113
|
-
state.finalReplySentTurn = turnCount;
|
|
114
|
-
state.finalReplySentAt = sentAt;
|
|
115
|
-
state.updatedAt = Date.now();
|
|
116
|
-
await writeStreamState(state);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function createEmptyStreamState(sessionId: string, cwd: string, tool: string, turnCount: number): StreamState {
|
|
120
|
-
return {
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isFinalReplySentForTurn(state: Pick<StreamState, "turnCount" | "finalReplySentTurn">): boolean {
|
|
107
|
+
return state.finalReplySentTurn === state.turnCount;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function markFinalReplySent(sessionId: string, turnCount: number, sentAt = Date.now()): Promise<void> {
|
|
111
|
+
const state = await readStreamState(sessionId);
|
|
112
|
+
if (!state) return;
|
|
113
|
+
if (state.turnCount !== turnCount) return;
|
|
114
|
+
if (state.status === "running") return;
|
|
115
|
+
|
|
116
|
+
state.finalReplySentTurn = turnCount;
|
|
117
|
+
state.finalReplySentAt = sentAt;
|
|
118
|
+
state.updatedAt = Date.now();
|
|
119
|
+
await writeStreamState(state);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createEmptyStreamState(sessionId: string, cwd: string, tool: string, turnCount: number): StreamState {
|
|
123
|
+
return {
|
|
121
124
|
sessionId,
|
|
122
125
|
status: "running",
|
|
123
126
|
accumulatedContent: "",
|
|
@@ -157,4 +160,4 @@ export async function fixStaleStreamStates(): Promise<void> {
|
|
|
157
160
|
} catch (err) {
|
|
158
161
|
console.error(`[${ts()}] [STREAM-STATE] fixStaleStreamStates failed: ${(err as Error).message}`);
|
|
159
162
|
}
|
|
160
|
-
}
|
|
163
|
+
}
|