chatccc 0.2.154 → 0.2.156
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 +2 -2
- package/package.json +1 -1
- package/src/__tests__/claude-adapter.test.ts +23 -0
- package/src/__tests__/session.test.ts +128 -25
- package/src/agent-stop-stuck.ts +2 -1
- package/src/config.ts +28 -4
- package/src/index.ts +6 -3
- package/src/orchestrator.ts +36 -12
- package/src/session-chat-binding.ts +6 -6
- package/src/session.ts +54 -32
- package/src/stream-state.ts +3 -0
- package/src/web-ui.ts +37 -0
package/config.sample.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"appSecret": ""
|
|
5
5
|
},
|
|
6
6
|
"platforms": {
|
|
7
|
-
"feishu": { "enabled": true },
|
|
7
|
+
"feishu": { "enabled": true, "platformType": "feishu" },
|
|
8
8
|
"ilink": { "enabled": true, "reuseTokenOnStart": true }
|
|
9
9
|
},
|
|
10
10
|
"port": 18080,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"effort": "",
|
|
19
19
|
"apiKey": "",
|
|
20
20
|
"baseUrl": "",
|
|
21
|
-
"maxTurn": 0
|
|
21
|
+
"maxTurn": 0
|
|
22
22
|
},
|
|
23
23
|
"cursor": {
|
|
24
24
|
"enabled": false,
|
package/package.json
CHANGED
|
@@ -459,6 +459,29 @@ describe("buildClaudePromptText", () => {
|
|
|
459
459
|
expect(result.endsWith("[User message]\nhello\n[/User message]")).toBe(true);
|
|
460
460
|
});
|
|
461
461
|
|
|
462
|
+
it("prepends the Claude-specific injection prompt on every resumed prompt", () => {
|
|
463
|
+
const first = buildClaudePromptText(
|
|
464
|
+
"[ChatCCC IM skill: feishu-skill]\ncapabilities\n[/ChatCCC IM skill: feishu-skill]\n\n[User message]\nfirst\n[/User message]",
|
|
465
|
+
"Never repeat successful commands for {{session_id}}.",
|
|
466
|
+
"sid-resume",
|
|
467
|
+
);
|
|
468
|
+
const second = buildClaudePromptText(
|
|
469
|
+
"[ChatCCC IM skill: feishu-skill]\ncapabilities\n[/ChatCCC IM skill: feishu-skill]\n\n[User message]\nsecond\n[/User message]",
|
|
470
|
+
"Never repeat successful commands for {{session_id}}.",
|
|
471
|
+
"sid-resume",
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
for (const result of [first, second]) {
|
|
475
|
+
expect(result).toContain("[ChatCCC Claude-specific injection prompt]");
|
|
476
|
+
expect(result).toContain("Never repeat successful commands for sid-resume.");
|
|
477
|
+
expect(result).toContain("[/ChatCCC Claude-specific injection prompt]");
|
|
478
|
+
expect(result).toContain("[ChatCCC IM skill: feishu-skill]");
|
|
479
|
+
expect(result).toContain("[/ChatCCC IM skill: feishu-skill]");
|
|
480
|
+
}
|
|
481
|
+
expect(first).toContain("[User message]\nfirst\n[/User message]");
|
|
482
|
+
expect(second).toContain("[User message]\nsecond\n[/User message]");
|
|
483
|
+
});
|
|
484
|
+
|
|
462
485
|
it("leaves user text unchanged when the injection prompt is empty", () => {
|
|
463
486
|
expect(buildClaudePromptText("hello", " ")).toBe("hello");
|
|
464
487
|
expect(buildClaudePromptText("hello", null)).toBe("hello");
|
|
@@ -9,9 +9,9 @@ const mockStreamStates = new Map<string, {
|
|
|
9
9
|
finalReply: string;
|
|
10
10
|
status?: "running" | "done" | "stopped" | "error";
|
|
11
11
|
turnCount?: number;
|
|
12
|
-
finalReplySentTurn?: number;
|
|
13
|
-
finalReplySentAt?: number;
|
|
14
|
-
}>();
|
|
12
|
+
finalReplySentTurn?: number;
|
|
13
|
+
finalReplySentAt?: number;
|
|
14
|
+
}>();
|
|
15
15
|
vi.mock("../stream-state.ts", () => ({
|
|
16
16
|
readStreamState: async (sid: string) => {
|
|
17
17
|
const state = mockStreamStates.get(sid);
|
|
@@ -20,9 +20,9 @@ vi.mock("../stream-state.ts", () => ({
|
|
|
20
20
|
sessionId: sid,
|
|
21
21
|
accumulatedContent: state.accumulatedContent,
|
|
22
22
|
finalReply: state.finalReply,
|
|
23
|
-
finalReplySentTurn: state.finalReplySentTurn,
|
|
24
|
-
finalReplySentAt: state.finalReplySentAt,
|
|
25
|
-
status: state.status ?? "running",
|
|
23
|
+
finalReplySentTurn: state.finalReplySentTurn,
|
|
24
|
+
finalReplySentAt: state.finalReplySentAt,
|
|
25
|
+
status: state.status ?? "running",
|
|
26
26
|
chunkCount: 0,
|
|
27
27
|
turnCount: state.turnCount ?? 0,
|
|
28
28
|
contextTokens: 0,
|
|
@@ -37,18 +37,18 @@ vi.mock("../stream-state.ts", () => ({
|
|
|
37
37
|
finalReply: string;
|
|
38
38
|
status?: "running" | "done" | "stopped" | "error";
|
|
39
39
|
turnCount?: number;
|
|
40
|
-
finalReplySentTurn?: number;
|
|
41
|
-
finalReplySentAt?: number;
|
|
42
|
-
}) => {
|
|
40
|
+
finalReplySentTurn?: number;
|
|
41
|
+
finalReplySentAt?: number;
|
|
42
|
+
}) => {
|
|
43
43
|
mockStreamStates.set(state.sessionId, {
|
|
44
44
|
accumulatedContent: state.accumulatedContent,
|
|
45
45
|
finalReply: state.finalReply,
|
|
46
46
|
status: state.status,
|
|
47
47
|
turnCount: state.turnCount,
|
|
48
|
-
finalReplySentTurn: state.finalReplySentTurn,
|
|
49
|
-
finalReplySentAt: state.finalReplySentAt,
|
|
50
|
-
});
|
|
51
|
-
},
|
|
48
|
+
finalReplySentTurn: state.finalReplySentTurn,
|
|
49
|
+
finalReplySentAt: state.finalReplySentAt,
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
52
|
createEmptyStreamState: (sid: string, cwd: string, tool: string, turnCount: number) => ({
|
|
53
53
|
sessionId: sid, status: "running" as const, accumulatedContent: "", finalReply: "", chunkCount: 0, turnCount, contextTokens: 0, updatedAt: Date.now(), cwd, tool,
|
|
54
54
|
}),
|
|
@@ -396,12 +396,48 @@ describe("runAgentSession process monitor", () => {
|
|
|
396
396
|
expect.stringContaining("进程异常结束"),
|
|
397
397
|
);
|
|
398
398
|
});
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
399
|
+
it("re-injects IM skill capabilities for each resumed Claude prompt", async () => {
|
|
400
|
+
const platform = mockPlatform("feishu");
|
|
401
|
+
setSessionPlatform(platform);
|
|
402
|
+
bindChatToSession("sid-resume", "chat-resume");
|
|
403
|
+
recordLastActiveChat("sid-resume", "chat-resume");
|
|
404
|
+
|
|
405
|
+
const sentTexts: string[] = [];
|
|
406
|
+
const adapter: ToolAdapter = {
|
|
407
|
+
displayName: "Claude Code",
|
|
408
|
+
sessionDescPrefix: "Claude Code Session:",
|
|
409
|
+
createSession: async () => ({ sessionId: "sid-resume" }),
|
|
410
|
+
getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "F:\\repo" }),
|
|
411
|
+
closeSession: async () => {},
|
|
412
|
+
prompt: async function* (_sid: string, text: string) {
|
|
413
|
+
sentTexts.push(text);
|
|
414
|
+
yield { type: "assistant", blocks: [{ type: "text", text: "done" }] };
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
_setAdapterForToolForTest("claude", adapter);
|
|
418
|
+
|
|
419
|
+
await runAgentSession("sid-resume", "first prompt", platform, "chat-resume", Date.now(), "claude");
|
|
420
|
+
await runAgentSession("sid-resume", "second prompt", platform, "chat-resume", Date.now(), "claude");
|
|
421
|
+
|
|
422
|
+
expect(sentTexts).toHaveLength(2);
|
|
423
|
+
for (const text of sentTexts) {
|
|
424
|
+
expect(text).toContain("[ChatCCC IM skill: feishu-skill]");
|
|
425
|
+
expect(text).toContain("[/ChatCCC IM skill: feishu-skill]");
|
|
426
|
+
expect(text).toContain('"session_id":"sid-resume"');
|
|
427
|
+
expect(text).toContain("http://127.0.0.1:");
|
|
428
|
+
expect(text).toContain("/api/agent/send-image");
|
|
429
|
+
expect(text).toContain("[User message]");
|
|
430
|
+
expect(text).toContain("[/User message]");
|
|
431
|
+
}
|
|
432
|
+
expect(sentTexts[0]).toContain("first prompt");
|
|
433
|
+
expect(sentTexts[1]).toContain("second prompt");
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
describe("unified display loop WeChat delta", () => {
|
|
438
|
+
beforeEach(() => {
|
|
439
|
+
vi.useFakeTimers();
|
|
440
|
+
resetState();
|
|
405
441
|
resetBindingState();
|
|
406
442
|
mockStreamStates.clear();
|
|
407
443
|
});
|
|
@@ -464,12 +500,79 @@ describe("unified display loop WeChat delta", () => {
|
|
|
464
500
|
2,
|
|
465
501
|
"chat-wechat",
|
|
466
502
|
"tool output",
|
|
467
|
-
);
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
describe("
|
|
472
|
-
|
|
503
|
+
);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
describe("unified display loop terminal card update", () => {
|
|
508
|
+
beforeEach(() => {
|
|
509
|
+
vi.useFakeTimers();
|
|
510
|
+
resetState();
|
|
511
|
+
resetBindingState();
|
|
512
|
+
mockStreamStates.clear();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
afterEach(() => {
|
|
516
|
+
stopUnifiedDisplayLoop();
|
|
517
|
+
resetBindingState();
|
|
518
|
+
vi.useRealTimers();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("does not repeat the same terminal CardKit sequence while the prompt is still active", async () => {
|
|
522
|
+
const platform = mockPlatform("feishu");
|
|
523
|
+
platform.cardUpdate = vi.fn(async () => {
|
|
524
|
+
throw new Error("CardKit update: [300317] ErrMsg: sequence number compare failed; ");
|
|
525
|
+
});
|
|
526
|
+
setSessionPlatform(platform);
|
|
527
|
+
|
|
528
|
+
bindChatToSession("sid-terminal", "chat-terminal");
|
|
529
|
+
recordLastActiveChat("sid-terminal", "chat-terminal");
|
|
530
|
+
sessionInfoMap.set("chat-terminal", {
|
|
531
|
+
sessionId: "sid-terminal",
|
|
532
|
+
turnCount: 1,
|
|
533
|
+
lastContextTokens: 0,
|
|
534
|
+
startTime: 0,
|
|
535
|
+
tool: "claude",
|
|
536
|
+
});
|
|
537
|
+
activePrompts.set("sid-terminal", {
|
|
538
|
+
controller: new AbortController(),
|
|
539
|
+
stopped: false,
|
|
540
|
+
startTime: Date.now(),
|
|
541
|
+
});
|
|
542
|
+
displayCards.set("chat-terminal", {
|
|
543
|
+
cardId: "card-terminal",
|
|
544
|
+
sequence: 109,
|
|
545
|
+
cardBusy: false,
|
|
546
|
+
cardCreatedAt: Date.now(),
|
|
547
|
+
lastSentContent: "",
|
|
548
|
+
streamErrorNotified: false,
|
|
549
|
+
sessionId: "sid-terminal",
|
|
550
|
+
turnCount: 1,
|
|
551
|
+
dotCount: 0,
|
|
552
|
+
});
|
|
553
|
+
mockStreamStates.set("sid-terminal", {
|
|
554
|
+
accumulatedContent: "partial tool output",
|
|
555
|
+
finalReply: "",
|
|
556
|
+
status: "stopped",
|
|
557
|
+
turnCount: 1,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
startUnifiedDisplayLoop();
|
|
561
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
562
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
563
|
+
|
|
564
|
+
expect(platform.cardUpdate).toHaveBeenCalledTimes(1);
|
|
565
|
+
expect(platform.cardUpdate).toHaveBeenCalledWith(
|
|
566
|
+
"card-terminal",
|
|
567
|
+
expect.any(String),
|
|
568
|
+
110,
|
|
569
|
+
);
|
|
570
|
+
expect(displayCards.get("chat-terminal")?.sequence).toBe(110);
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
describe("rebuildBindingsFromRegistry", () => {
|
|
575
|
+
let registryFile = "";
|
|
473
576
|
|
|
474
577
|
beforeEach(async () => {
|
|
475
578
|
chatSessionMap.clear();
|
package/src/agent-stop-stuck.ts
CHANGED
|
@@ -94,6 +94,7 @@ export async function handleAgentStopStuckRequest(
|
|
|
94
94
|
await writeStreamState({
|
|
95
95
|
...current,
|
|
96
96
|
status: "done",
|
|
97
|
+
stuckAt: Date.now(),
|
|
97
98
|
finalReply: finalReply ? current.finalReply + "\n\n" + finalReply : current.finalReply,
|
|
98
99
|
updatedAt: Date.now(),
|
|
99
100
|
});
|
|
@@ -113,4 +114,4 @@ export async function handleAgentStopStuckRequest(
|
|
|
113
114
|
|
|
114
115
|
jsonReply(res, 200, { ok: true });
|
|
115
116
|
return true;
|
|
116
|
-
}
|
|
117
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -102,6 +102,8 @@ export interface FeishuConfig {
|
|
|
102
102
|
export interface PlatformConfig {
|
|
103
103
|
enabled: boolean;
|
|
104
104
|
reuseTokenOnStart?: boolean;
|
|
105
|
+
/** 飞书平台类型:feishu(国内飞书)或 lark(国际版);缺省按 feishu */
|
|
106
|
+
platformType?: "feishu" | "lark";
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
export interface PlatformsConfig {
|
|
@@ -319,6 +321,11 @@ function autofillToolPathsAfterSampleCopy(configFile: string): void {
|
|
|
319
321
|
}
|
|
320
322
|
}
|
|
321
323
|
|
|
324
|
+
function normalizePlatformType(raw: string): "feishu" | "lark" {
|
|
325
|
+
if (raw === "lark") return "lark";
|
|
326
|
+
return "feishu";
|
|
327
|
+
}
|
|
328
|
+
|
|
322
329
|
function loadConfig(): AppConfig {
|
|
323
330
|
const defaults: AppConfig = {
|
|
324
331
|
feishu: { appId: "", appSecret: "" },
|
|
@@ -449,6 +456,11 @@ function loadConfig(): AppConfig {
|
|
|
449
456
|
enabled: typeof (parsed.platforms as unknown as Record<string, unknown> | undefined)?.feishu === "object"
|
|
450
457
|
? Boolean(((parsed.platforms as unknown as Record<string, unknown>).feishu as Record<string, unknown>).enabled ?? true)
|
|
451
458
|
: true,
|
|
459
|
+
platformType: normalizePlatformType(
|
|
460
|
+
typeof (parsed.platforms as unknown as Record<string, unknown> | undefined)?.feishu === "object"
|
|
461
|
+
? String(((parsed.platforms as unknown as Record<string, unknown>).feishu as Record<string, unknown>).platformType ?? "feishu")
|
|
462
|
+
: "feishu",
|
|
463
|
+
),
|
|
452
464
|
},
|
|
453
465
|
ilink: {
|
|
454
466
|
enabled: typeof (parsed.platforms as unknown as Record<string, unknown> | undefined)?.ilink === "object"
|
|
@@ -517,7 +529,14 @@ export let APP_SECRET = config.feishu.appSecret;
|
|
|
517
529
|
export let FEISHU_ENABLED = config.platforms.feishu.enabled;
|
|
518
530
|
export let ILINK_ENABLED = config.platforms.ilink.enabled;
|
|
519
531
|
export let ILINK_REUSE_TOKEN_ON_START = config.platforms.ilink.reuseTokenOnStart ?? true;
|
|
520
|
-
|
|
532
|
+
|
|
533
|
+
function computeBaseUrl(platformType?: string): string {
|
|
534
|
+
if (platformType === "lark") return "https://open.larksuite.com/open-apis";
|
|
535
|
+
return "https://open.feishu.cn/open-apis";
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export let BASE_URL = computeBaseUrl(config.platforms.feishu.platformType);
|
|
539
|
+
export let FEISHU_PLATFORM_TYPE: "feishu" | "lark" = config.platforms.feishu.platformType === "lark" ? "lark" : "feishu";
|
|
521
540
|
export const CHATCCC_PORT = config.port;
|
|
522
541
|
|
|
523
542
|
/** 与 CHATCCC_PORT 一致,供 --local 连接本机中继 */
|
|
@@ -603,11 +622,13 @@ export function applyLoadedConfig(next: AppConfig): void {
|
|
|
603
622
|
FEISHU_ENABLED = next.platforms.feishu.enabled;
|
|
604
623
|
ILINK_ENABLED = next.platforms.ilink.enabled;
|
|
605
624
|
ILINK_REUSE_TOKEN_ON_START = next.platforms.ilink.reuseTokenOnStart ?? true;
|
|
625
|
+
FEISHU_PLATFORM_TYPE = next.platforms.feishu.platformType === "lark" ? "lark" : "feishu";
|
|
626
|
+
BASE_URL = computeBaseUrl(FEISHU_PLATFORM_TYPE);
|
|
606
627
|
CLAUDE_MODEL = next.claude.model;
|
|
607
628
|
CLAUDE_SUBAGENT_MODEL = next.claude.subagentModel;
|
|
608
|
-
CLAUDE_EFFORT = next.claude.effort;
|
|
609
|
-
CLAUDE_MAX_TURN = next.claude.maxTurn;
|
|
610
|
-
CLAUDE_API_KEY = next.claude.apiKey;
|
|
629
|
+
CLAUDE_EFFORT = next.claude.effort;
|
|
630
|
+
CLAUDE_MAX_TURN = next.claude.maxTurn;
|
|
631
|
+
CLAUDE_API_KEY = next.claude.apiKey;
|
|
611
632
|
CLAUDE_BASE_URL = next.claude.baseUrl;
|
|
612
633
|
GIT_TIMEOUT_SECONDS = next.gitTimeoutSeconds;
|
|
613
634
|
GIT_TIMEOUT_MS = GIT_TIMEOUT_SECONDS * 1000;
|
|
@@ -738,6 +759,9 @@ export function reportEnvironmentVariableReadout(): void {
|
|
|
738
759
|
APP_SECRET.trim() ? "已读入(内容不在日志中显示)" : "未读入或为空"
|
|
739
760
|
);
|
|
740
761
|
|
|
762
|
+
console.log(` [默认] [可选] platforms.feishu.platformType`);
|
|
763
|
+
console.log(` 平台类型: ${FEISHU_PLATFORM_TYPE === "lark" ? "Lark (open.larksuite.com)" : "飞书 (open.feishu.cn)"}`);
|
|
764
|
+
|
|
741
765
|
console.log(` [默认] [可选] port`);
|
|
742
766
|
console.log(` 监听端口: ${CHATCCC_PORT}`);
|
|
743
767
|
|
package/src/index.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
26
26
|
|
|
27
|
-
import { WSClient, EventDispatcher } from "@larksuiteoapi/node-sdk";
|
|
27
|
+
import { WSClient, EventDispatcher, Domain } from "@larksuiteoapi/node-sdk";
|
|
28
28
|
import WebSocket from "ws";
|
|
29
29
|
|
|
30
30
|
import { appendStartupTrace, attachRelayWebSocket, ensureSingleInstance, freeRelayListenPort, installCrashLogging, waitForPortFree } from "./shared.ts";
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
APP_ID,
|
|
37
37
|
APP_SECRET,
|
|
38
38
|
FEISHU_ENABLED,
|
|
39
|
+
FEISHU_PLATFORM_TYPE,
|
|
39
40
|
ILINK_ENABLED,
|
|
40
41
|
ILINK_REUSE_TOKEN_ON_START,
|
|
41
42
|
BASE_URL,
|
|
@@ -212,7 +213,7 @@ function parseCardAction(data: unknown): CardActionResult | null {
|
|
|
212
213
|
}
|
|
213
214
|
if (!cmd) return null;
|
|
214
215
|
|
|
215
|
-
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", state: "/state", cd: "/cd", sessions: "/sessions", newh: "/newh" };
|
|
216
|
+
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", state: "/state", cd: "/cd", sessions: "/sessions", newh: "/newh" };
|
|
216
217
|
let text = CMD_MAP[cmd] ?? "";
|
|
217
218
|
if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
|
|
218
219
|
const path = (action.value as Record<string, string>).path;
|
|
@@ -348,9 +349,10 @@ async function startBotServiceCore(): Promise<void> {
|
|
|
348
349
|
const msg = err instanceof Error ? err.message : String(err);
|
|
349
350
|
console.error(" 失败:无法获取 tenant_access_token。");
|
|
350
351
|
console.error(` 接口: POST ${BASE_URL}/auth/v3/tenant_access_token/internal`);
|
|
352
|
+
const apiHost = BASE_URL.replace("https://", "").replace("/open-apis", "");
|
|
351
353
|
console.error(" 常见原因:");
|
|
352
354
|
console.error(
|
|
353
|
-
|
|
355
|
+
` - 本机网络无法访问 ${apiHost}(可尝试:关闭系统/终端代理、检查防火墙;Windows 可管理员运行 netsh winsock reset 后重启)`,
|
|
354
356
|
);
|
|
355
357
|
console.error(" - App ID / App Secret 与开放平台「凭证与基础信息」不一致");
|
|
356
358
|
console.error(" - 自建应用尚未创建/发布可用版本");
|
|
@@ -542,6 +544,7 @@ async function startBotServiceCore(): Promise<void> {
|
|
|
542
544
|
const wsClient = new WSClient({
|
|
543
545
|
appId: APP_ID,
|
|
544
546
|
appSecret: APP_SECRET,
|
|
547
|
+
domain: FEISHU_PLATFORM_TYPE === "lark" ? Domain.Lark : Domain.Feishu,
|
|
545
548
|
onReady: async () => {
|
|
546
549
|
await rebuildBindingsFromRegistry().catch((err) =>
|
|
547
550
|
console.error(`[${ts()}] [SDK READY] rebuild bindings failed: ${(err as Error).message}`)
|
package/src/orchestrator.ts
CHANGED
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
cancelQueuedMessage,
|
|
79
79
|
} from "./session-chat-binding.ts";
|
|
80
80
|
import { getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
|
|
81
|
+
import { readStreamState } from "./stream-state.ts";
|
|
81
82
|
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
82
83
|
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
83
84
|
|
|
@@ -593,10 +594,12 @@ export async function handleCommand(
|
|
|
593
594
|
|
|
594
595
|
if (sessionId && descriptionTool && toolLabel) {
|
|
595
596
|
// 有会话上下文 — 路由到命令处理或 prompt
|
|
596
|
-
logTrace(tid, "BRANCH", { sessionId, tool: descriptionTool });
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
597
|
+
logTrace(tid, "BRANCH", { sessionId, tool: descriptionTool });
|
|
598
|
+
const routeKind = textLower.startsWith("/") ? "command" : "prompt";
|
|
599
|
+
const chatKind = chatType === "p2p" ? "p2p chat" : "session group";
|
|
600
|
+
console.log(
|
|
601
|
+
`[${ts()}] [ROUTE] ${toolLabel} ${chatKind} ${routeKind} detected, session=${sessionId} tool=${descriptionTool}`,
|
|
602
|
+
);
|
|
600
603
|
|
|
601
604
|
if (
|
|
602
605
|
chatType !== "p2p" &&
|
|
@@ -1228,6 +1231,27 @@ export async function handleCommand(
|
|
|
1228
1231
|
await platform.sendText(chatId, "生成中...").catch(() => {});
|
|
1229
1232
|
}
|
|
1230
1233
|
|
|
1234
|
+
// 检查 session 是否被 stop-stuck-loop 标记为卡住,是则创建新 session
|
|
1235
|
+
const stuckState = await readStreamState(sessionId);
|
|
1236
|
+
if (stuckState?.stuckAt) {
|
|
1237
|
+
try {
|
|
1238
|
+
const init = await initClaudeSession(descriptionTool, undefined, chatId);
|
|
1239
|
+
const newSessionId = init.sessionId;
|
|
1240
|
+
unbindChatFromSession(sessionId, chatId);
|
|
1241
|
+
bindChatToSession(newSessionId, chatId);
|
|
1242
|
+
await recordSessionRegistry({
|
|
1243
|
+
chatId,
|
|
1244
|
+
sessionId: newSessionId,
|
|
1245
|
+
tool: descriptionTool,
|
|
1246
|
+
chatName: sessionChatName(text.slice(0, 10), init.cwd),
|
|
1247
|
+
}).catch(() => {});
|
|
1248
|
+
console.log(`[${ts()}] [STUCK-REDIRECT] Session ${sessionId} was stuck, created new session ${newSessionId}`);
|
|
1249
|
+
sessionId = newSessionId;
|
|
1250
|
+
} catch (err) {
|
|
1251
|
+
console.warn(`[${ts()}] [STUCK-REDIRECT] Failed to create new session: ${(err as Error).message}`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1231
1255
|
try {
|
|
1232
1256
|
logTrace(tid, "RESUME", { sessionId, tool: descriptionTool });
|
|
1233
1257
|
await resumeAndPrompt(
|
|
@@ -1235,10 +1259,10 @@ export async function handleCommand(
|
|
|
1235
1259
|
text,
|
|
1236
1260
|
platform,
|
|
1237
1261
|
chatId,
|
|
1238
|
-
msgTimestamp,
|
|
1239
|
-
descriptionTool,
|
|
1240
|
-
tid,
|
|
1241
|
-
);
|
|
1262
|
+
msgTimestamp,
|
|
1263
|
+
descriptionTool,
|
|
1264
|
+
tid,
|
|
1265
|
+
);
|
|
1242
1266
|
logTrace(tid, "DONE", { outcome: "resume_done", sessionId });
|
|
1243
1267
|
console.log(`[${ts()}] [RESUME] Session ${sessionId} done`);
|
|
1244
1268
|
} catch (err) {
|
|
@@ -1434,10 +1458,10 @@ export async function handleCommand(
|
|
|
1434
1458
|
text,
|
|
1435
1459
|
platform,
|
|
1436
1460
|
newChatId,
|
|
1437
|
-
msgTimestamp,
|
|
1438
|
-
tool,
|
|
1439
|
-
tid,
|
|
1440
|
-
);
|
|
1461
|
+
msgTimestamp,
|
|
1462
|
+
tool,
|
|
1463
|
+
tid,
|
|
1464
|
+
);
|
|
1441
1465
|
console.log(`[${ts()}] [AUTO-P2P 5/5] First prompt sent → OK`);
|
|
1442
1466
|
logTrace(tid, "DONE", { outcome: "auto_new_p2p_prompt_done", newChatId, sessionId, tool });
|
|
1443
1467
|
} catch (err) {
|
|
@@ -71,11 +71,11 @@ export interface ActivePrompt {
|
|
|
71
71
|
/** Set when the watchdog detects that the CLI process disappeared before stream finalization. */
|
|
72
72
|
abnormalExit?: boolean;
|
|
73
73
|
abnormalExitNotified?: boolean;
|
|
74
|
-
/** Set when the resource monitor detects CPU + memory unchanged for 3 minutes. */
|
|
75
|
-
resourceStuck?: boolean;
|
|
76
|
-
/** Adapter-provided callback to close the underlying SDK session / subprocess.
|
|
77
|
-
* Called by stop-stuck-loop before controller.abort() to terminate the CLI
|
|
78
|
-
* process immediately, rather than waiting for the async generator to unblock. */
|
|
74
|
+
/** Set when the resource monitor detects CPU + memory unchanged for 3 minutes. */
|
|
75
|
+
resourceStuck?: boolean;
|
|
76
|
+
/** Adapter-provided callback to close the underlying SDK session / subprocess.
|
|
77
|
+
* Called by stop-stuck-loop before controller.abort() to terminate the CLI
|
|
78
|
+
* process immediately, rather than waiting for the async generator to unblock. */
|
|
79
79
|
closeSession?: () => void;
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -228,4 +228,4 @@ export function resetBindingState(): void {
|
|
|
228
228
|
clearInterval(unifiedDisplayLoopHandle);
|
|
229
229
|
unifiedDisplayLoopHandle = null;
|
|
230
230
|
}
|
|
231
|
-
}
|
|
231
|
+
}
|
package/src/session.ts
CHANGED
|
@@ -167,11 +167,15 @@ function formatTerminalHeader(status: "running" | "done" | "stopped" | "error"):
|
|
|
167
167
|
return { title: "完成" };
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
function turnFinalStatus(status: "running" | "done" | "stopped" | "error"): "done" | "stopped" {
|
|
171
|
-
return status === "stopped" || status === "error" ? "stopped" : "done";
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function
|
|
170
|
+
function turnFinalStatus(status: "running" | "done" | "stopped" | "error"): "done" | "stopped" {
|
|
171
|
+
return status === "stopped" || status === "error" ? "stopped" : "done";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isCardKitSequenceConflict(err: unknown): boolean {
|
|
175
|
+
return err instanceof Error && err.message.includes("300317");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function startPromptProcessMonitor(sessionId: string, info: ToolProcessInfo): void {
|
|
175
179
|
const prompt = activePrompts.get(sessionId);
|
|
176
180
|
if (!prompt) return;
|
|
177
181
|
prompt.processPid = info.pid;
|
|
@@ -1157,11 +1161,11 @@ export async function runAgentSession(
|
|
|
1157
1161
|
// 标记 prompt 结束
|
|
1158
1162
|
resourceMonitor.off("stuck", onResourceStuck);
|
|
1159
1163
|
const prompt = activePrompts.get(sessionId);
|
|
1160
|
-
const wasStopped = prompt?.stopped ?? false;
|
|
1161
|
-
const wasAbnormalExit = prompt?.abnormalExit ?? false;
|
|
1162
|
-
const wasResourceStuck = prompt?.resourceStuck ?? false;
|
|
1163
|
-
clearPromptProcessMonitor(sessionId);
|
|
1164
|
-
activePrompts.delete(sessionId);
|
|
1164
|
+
const wasStopped = prompt?.stopped ?? false;
|
|
1165
|
+
const wasAbnormalExit = prompt?.abnormalExit ?? false;
|
|
1166
|
+
const wasResourceStuck = prompt?.resourceStuck ?? false;
|
|
1167
|
+
clearPromptProcessMonitor(sessionId);
|
|
1168
|
+
activePrompts.delete(sessionId);
|
|
1165
1169
|
|
|
1166
1170
|
// 先写最终状态(done/stopped),确保 display loop 在下一轮消费前
|
|
1167
1171
|
// 读到新状态并终结旧卡片。否则 setImmediate 在 CHECK 阶段先于
|
|
@@ -1172,13 +1176,13 @@ export async function runAgentSession(
|
|
|
1172
1176
|
|
|
1173
1177
|
// stop-stuck-loop 接口可能在 fire-and-forget 中已写入带 final_reply 的
|
|
1174
1178
|
// stream state,finally 不应覆盖它。
|
|
1175
|
-
let finalReplyToWrite = finalReply;
|
|
1176
|
-
try {
|
|
1177
|
-
const existing = await readStreamState(sessionId);
|
|
1178
|
-
if (existing && existing.finalReply.length > finalReply.length) {
|
|
1179
|
-
finalReplyToWrite = existing.finalReply;
|
|
1180
|
-
}
|
|
1181
|
-
} catch {}
|
|
1179
|
+
let finalReplyToWrite = finalReply;
|
|
1180
|
+
try {
|
|
1181
|
+
const existing = await readStreamState(sessionId);
|
|
1182
|
+
if (existing && existing.finalReply.length > finalReply.length) {
|
|
1183
|
+
finalReplyToWrite = existing.finalReply;
|
|
1184
|
+
}
|
|
1185
|
+
} catch {}
|
|
1182
1186
|
|
|
1183
1187
|
await writeStreamState({
|
|
1184
1188
|
sessionId,
|
|
@@ -1187,11 +1191,11 @@ export async function runAgentSession(
|
|
|
1187
1191
|
finalReply: finalReplyToWrite,
|
|
1188
1192
|
chunkCount: state.chunkCount,
|
|
1189
1193
|
turnCount: nextTurnCount,
|
|
1190
|
-
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
1191
|
-
updatedAt: Date.now(),
|
|
1192
|
-
cwd,
|
|
1193
|
-
tool,
|
|
1194
|
-
});
|
|
1194
|
+
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
1195
|
+
updatedAt: Date.now(),
|
|
1196
|
+
cwd,
|
|
1197
|
+
tool,
|
|
1198
|
+
});
|
|
1195
1199
|
|
|
1196
1200
|
// 消费队列中的缓存消息(异步,不阻塞后续清理)
|
|
1197
1201
|
// 用户 /stop 后应丢弃队列消息,避免用户停止后又自动开始新轮
|
|
@@ -1356,24 +1360,42 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1356
1360
|
displayCards.delete(chatId);
|
|
1357
1361
|
} else {
|
|
1358
1362
|
// 发送最终结果(卡片平台)
|
|
1359
|
-
while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
|
|
1360
|
-
const
|
|
1363
|
+
while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
|
|
1364
|
+
const promptStillActive = activePrompts.has(sessionId);
|
|
1365
|
+
if (
|
|
1366
|
+
promptStillActive &&
|
|
1367
|
+
display.lastSentAccLen === state.accumulatedContent.length &&
|
|
1368
|
+
display.lastSentFinalReply === state.finalReply
|
|
1369
|
+
) {
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
const nextSeq = display.sequence + 1;
|
|
1361
1373
|
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status);
|
|
1362
1374
|
const cardContent = truncateContent(state.accumulatedContent + state.finalReply) || " ";
|
|
1363
1375
|
const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1376
|
+
let terminalCardUpdateAccepted = false;
|
|
1377
|
+
await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
|
|
1378
|
+
display.sequence = nextSeq;
|
|
1379
|
+
terminalCardUpdateAccepted = true;
|
|
1380
|
+
}).catch(err => {
|
|
1381
|
+
console.error(`[${ts()}] [DISPLAY] terminal cardUpdate failed: ${(err as Error).message}`);
|
|
1382
|
+
if (isCardKitSequenceConflict(err)) {
|
|
1383
|
+
display.sequence = nextSeq;
|
|
1384
|
+
terminalCardUpdateAccepted = true;
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1367
1387
|
|
|
1368
1388
|
// 若 session 仍在 activePrompts 中,说明 runAgentSession 的 finally
|
|
1369
1389
|
// 还没执行,当前 stream state 可能是 stopSession fire-and-forget
|
|
1370
1390
|
// 写入的,finalReply 滞后于内存态。卡片已更新为终态外观,但不发送
|
|
1371
1391
|
// 文本、不删除 display 条目,留给 finally 落盘后的下一次 tick 处理。
|
|
1372
|
-
if (
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1392
|
+
if (promptStillActive) {
|
|
1393
|
+
if (terminalCardUpdateAccepted) {
|
|
1394
|
+
display.lastSentAccLen = state.accumulatedContent.length;
|
|
1395
|
+
display.lastSentFinalReply = state.finalReply;
|
|
1396
|
+
}
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1377
1399
|
|
|
1378
1400
|
const finalSt = turnFinalStatus(state.status);
|
|
1379
1401
|
finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
|
package/src/stream-state.ts
CHANGED
|
@@ -26,6 +26,9 @@ export interface StreamState {
|
|
|
26
26
|
updatedAt: number;
|
|
27
27
|
cwd: string;
|
|
28
28
|
tool: string;
|
|
29
|
+
/** Set by stop-stuck-loop to prevent the session from being resumed.
|
|
30
|
+
* The orchestrator checks this before resuming and creates a new session instead. */
|
|
31
|
+
stuckAt?: number;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
function getStreamStatePath(sessionId: string): string {
|
package/src/web-ui.ts
CHANGED
|
@@ -296,6 +296,10 @@ export function unflattenConfig(flat: Record<string, unknown>): Record<string, u
|
|
|
296
296
|
result.platforms = result.platforms || {};
|
|
297
297
|
(result.platforms as Record<string, unknown>).feishu = (result.platforms as Record<string, unknown>).feishu || {};
|
|
298
298
|
((result.platforms as Record<string, unknown>).feishu as Record<string, unknown>).enabled = val === true || val === "true";
|
|
299
|
+
} else if (key === "CHATCCC_FEISHU_PLATFORM_TYPE") {
|
|
300
|
+
result.platforms = result.platforms || {};
|
|
301
|
+
(result.platforms as Record<string, unknown>).feishu = (result.platforms as Record<string, unknown>).feishu || {};
|
|
302
|
+
((result.platforms as Record<string, unknown>).feishu as Record<string, unknown>).platformType = val;
|
|
299
303
|
} else if (key === "CHATCCC_ILINK_ENABLED") {
|
|
300
304
|
result.platforms = result.platforms || {};
|
|
301
305
|
(result.platforms as Record<string, unknown>).ilink = (result.platforms as Record<string, unknown>).ilink || {};
|
|
@@ -582,6 +586,14 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
582
586
|
<input type="checkbox" class="agent-toggle" id="platform-enable-feishu" checked onchange="onWizardPlatformToggle('feishu', this.checked)">
|
|
583
587
|
</div>
|
|
584
588
|
<div id="feishu-cred-fields" style="margin-top:12px;padding-top:12px;border-top:1px solid #e2e8f0">
|
|
589
|
+
<div class="form-group" style="margin-bottom:10px">
|
|
590
|
+
<label>平台类型</label>
|
|
591
|
+
<select id="field-CHATCCC_FEISHU_PLATFORM_TYPE" style="width:100%;padding:8px 12px;border:1px solid #cbd5e1;border-radius:8px;font-size:14px;outline:none">
|
|
592
|
+
<option value="feishu">飞书 (open.feishu.cn)</option>
|
|
593
|
+
<option value="lark">Lark (open.larksuite.com)</option>
|
|
594
|
+
</select>
|
|
595
|
+
<div class="hint">飞书或 Lark 国际版,决定 API 服务器地址</div>
|
|
596
|
+
</div>
|
|
585
597
|
<div class="form-group" style="margin-bottom:10px">
|
|
586
598
|
<label>CHATCCC_APP_ID *</label>
|
|
587
599
|
<input type="text" id="field-CHATCCC_APP_ID" placeholder="cli_xxxxxxxxxxxx">
|
|
@@ -759,6 +771,7 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
759
771
|
</div>
|
|
760
772
|
<div class="config-row"><span class="key">App ID</span><span class="val" id="cfg-APP_ID">-</span></div>
|
|
761
773
|
<div class="config-row"><span class="key">App Secret</span><span class="val" id="cfg-APP_SECRET">-</span></div>
|
|
774
|
+
<div class="config-row"><span class="key">平台类型</span><span class="val" id="cfg-FEISHU_PLATFORM_TYPE">-</span></div>
|
|
762
775
|
<button class="btn btn-outline" style="margin-top:8px" onclick="editSection('feishu')">编辑</button>
|
|
763
776
|
</div>
|
|
764
777
|
</details>
|
|
@@ -1058,6 +1071,8 @@ function renderStep1() {
|
|
|
1058
1071
|
var f = c.feishu || {};
|
|
1059
1072
|
prefillNested('field-CHATCCC_APP_ID', f.appId);
|
|
1060
1073
|
prefillNested('field-CHATCCC_APP_SECRET', f.appSecret);
|
|
1074
|
+
var pf = (c.platforms && c.platforms.feishu) || {};
|
|
1075
|
+
prefillNested('field-CHATCCC_FEISHU_PLATFORM_TYPE', pf.platformType || 'feishu');
|
|
1061
1076
|
// 平台开关:按已有 config 回填;首次配置(无飞书凭证)时默认关闭飞书、开启微信
|
|
1062
1077
|
var hasExistingCreds = Boolean(c.feishu?.appId?.trim() && c.feishu?.appSecret?.trim());
|
|
1063
1078
|
var feishuEnabled = hasExistingCreds
|
|
@@ -1156,6 +1171,9 @@ function collectAllFields() {
|
|
|
1156
1171
|
var el = document.getElementById('field-' + key);
|
|
1157
1172
|
if (el && el.value.trim()) vars[key] = el.value.trim();
|
|
1158
1173
|
});
|
|
1174
|
+
// 平台类型(下拉选择框,始终发送以保持与服务端同步)
|
|
1175
|
+
var ptEl = document.getElementById('field-CHATCCC_FEISHU_PLATFORM_TYPE');
|
|
1176
|
+
if (ptEl && ptEl.value.trim()) vars['CHATCCC_FEISHU_PLATFORM_TYPE'] = ptEl.value.trim();
|
|
1159
1177
|
vars.CHATCCC_FEISHU_ENABLED = !!state.platformsEnabled.feishu;
|
|
1160
1178
|
vars.CHATCCC_ILINK_ENABLED = !!state.platformsEnabled.ilink;
|
|
1161
1179
|
vars.CHATCCC_CLAUDE_ENABLED = !!state.agentsEnabled.claude;
|
|
@@ -1197,6 +1215,8 @@ function renderStep3() {
|
|
|
1197
1215
|
if (state.platformsEnabled.feishu) {
|
|
1198
1216
|
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>');
|
|
1199
1217
|
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>');
|
|
1218
|
+
var ptLabel = vars.CHATCCC_FEISHU_PLATFORM_TYPE === 'lark' ? 'Lark (open.larksuite.com)' : '飞书 (open.feishu.cn)';
|
|
1219
|
+
lines.push('<div class="config-row"><span class="key">平台类型</span><span class="val">' + ptLabel + '</span></div>');
|
|
1200
1220
|
}
|
|
1201
1221
|
|
|
1202
1222
|
lines.push('<h3 style="margin:16px 0 8px">微信 iLink</h3>');
|
|
@@ -1368,6 +1388,8 @@ function updateDashboardUI() {
|
|
|
1368
1388
|
state.platformsEnabled.feishu = feishuEnabled;
|
|
1369
1389
|
document.getElementById('cfg-APP_ID').textContent = c.feishu && c.feishu.appId ? c.feishu.appId.slice(0,8) + '...' + c.feishu.appId.slice(-4) : '-';
|
|
1370
1390
|
document.getElementById('cfg-APP_SECRET').textContent = c.feishu && c.feishu.appSecret ? '***已设置***' : '-';
|
|
1391
|
+
var pt = (c.platforms && c.platforms.feishu && c.platforms.feishu.platformType === 'lark') ? 'Lark (open.larksuite.com)' : '飞书 (open.feishu.cn)';
|
|
1392
|
+
document.getElementById('cfg-FEISHU_PLATFORM_TYPE').textContent = pt;
|
|
1371
1393
|
|
|
1372
1394
|
// 只显示已启用的 Agent 卡片(按 enabled 字段;缺省时退回到"任一字段非空"兼容旧 config)
|
|
1373
1395
|
var claudeOn = isAgentEnabled(c.claude, CLAUDE_FALLBACK_KEYS);
|
|
@@ -1497,6 +1519,16 @@ function editSection(section) {
|
|
|
1497
1519
|
html += '<input type="' + (isSecret ? 'password' : 'text') + '" id="edit-' + key + '" value="' + String(val).replace(/"/g,'"') + '">';
|
|
1498
1520
|
html += '</div>';
|
|
1499
1521
|
});
|
|
1522
|
+
// 平台类型:feishu 编辑时额外渲染下拉选择框
|
|
1523
|
+
if (section === 'feishu') {
|
|
1524
|
+
var ptVal = (state.config.platforms && state.config.platforms.feishu && state.config.platforms.feishu.platformType) || 'feishu';
|
|
1525
|
+
html += '<div class="form-group"><label>平台类型</label>';
|
|
1526
|
+
html += '<select id="edit-CHATCCC_FEISHU_PLATFORM_TYPE" style="width:100%;padding:8px 12px;border:1px solid #cbd5e1;border-radius:8px;font-size:14px;outline:none">';
|
|
1527
|
+
html += '<option value="feishu"' + (ptVal === 'feishu' ? ' selected' : '') + '>飞书 (open.feishu.cn)</option>';
|
|
1528
|
+
html += '<option value="lark"' + (ptVal === 'lark' ? ' selected' : '') + '>Lark (open.larksuite.com)</option>';
|
|
1529
|
+
html += '</select></div>';
|
|
1530
|
+
}
|
|
1531
|
+
document.getElementById('edit-modal-fields').innerHTML = html;
|
|
1500
1532
|
}
|
|
1501
1533
|
|
|
1502
1534
|
function closeEditModal() {
|
|
@@ -1515,6 +1547,11 @@ async function saveEdit() {
|
|
|
1515
1547
|
var el = document.getElementById('edit-' + key);
|
|
1516
1548
|
if (el) vars[key] = el.value.trim();
|
|
1517
1549
|
});
|
|
1550
|
+
// 平台类型:feishu 编辑时额外采集下拉选择框的值
|
|
1551
|
+
if (editSectionType === 'feishu') {
|
|
1552
|
+
var ptEl = document.getElementById('edit-CHATCCC_FEISHU_PLATFORM_TYPE');
|
|
1553
|
+
if (ptEl && ptEl.value.trim()) vars['CHATCCC_FEISHU_PLATFORM_TYPE'] = ptEl.value.trim();
|
|
1554
|
+
}
|
|
1518
1555
|
await saveConfig(vars);
|
|
1519
1556
|
closeEditModal();
|
|
1520
1557
|
updateDashboardUI();
|