chatccc 0.2.289 → 0.2.291
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.
|
@@ -1,26 +1,109 @@
|
|
|
1
1
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* 会话级能力凭据(capability grant)。
|
|
7
|
+
*
|
|
8
|
+
* 内存 Map 是本进程的权威视图;磁盘只用于跨进程重启恢复。
|
|
9
|
+
*
|
|
10
|
+
* 两条必须遵守的约束:
|
|
11
|
+
* 1. 签发后要落盘、校验时先内存后磁盘。否则 ChatCCC 重启会让所有定时发送
|
|
12
|
+
* (周期报告、后台任务)在重启后立刻收到 403,而工作负载本身仍在运行 ——
|
|
13
|
+
* 故障是静默的。
|
|
14
|
+
* 2. clearAgentCapabilityGrants() 只清内存缓存,**绝不**删盘。它在进程首次启动
|
|
15
|
+
* 的 resetState() 里被调用;若此处删盘,每次重启都会重新随机发码,本修复
|
|
16
|
+
* 直接失效。需要真正作废授权请用 revokeAgentCapabilityGrant()。
|
|
17
|
+
*/
|
|
2
18
|
const grantsBySession = new Map();
|
|
19
|
+
/**
|
|
20
|
+
* 与 config.ts 的 USER_DATA_DIR 同源。这里刻意不 import config.ts:该模块在顶层
|
|
21
|
+
* 有副作用(初始化文件日志、加载配置),被测试间接导入会带来多余 I/O 与循环
|
|
22
|
+
* 依赖风险;重复一个路径构造比引入副作用更安全。
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_GRANTS_DIR = join(homedir(), ".chatccc", "state", "agent-grants");
|
|
25
|
+
/** 覆盖点(测试/运维);在每次调用时读取,便于测试导入之后再设置。 */
|
|
26
|
+
export function agentGrantDir() {
|
|
27
|
+
const override = process.env.CHATCCC_AGENT_GRANT_DIR;
|
|
28
|
+
return override !== undefined && override.trim() !== "" ? override : DEFAULT_GRANTS_DIR;
|
|
29
|
+
}
|
|
30
|
+
function grantFilePath(sessionId) {
|
|
31
|
+
// sessionId 会进入文件名:只保留安全字符,避免路径穿越。
|
|
32
|
+
const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
33
|
+
return join(agentGrantDir(), `${safe}.grant`);
|
|
34
|
+
}
|
|
35
|
+
function readPersistedGrant(sessionId) {
|
|
36
|
+
try {
|
|
37
|
+
const value = readFileSync(grantFilePath(sessionId), "utf8").trim();
|
|
38
|
+
return value === "" ? undefined : value;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined; // 不存在 / 不可读都视作“没有持久化授权”
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function persistGrant(sessionId, grant) {
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(agentGrantDir(), { recursive: true });
|
|
47
|
+
const target = grantFilePath(sessionId);
|
|
48
|
+
const tmp = `${target}.tmp`;
|
|
49
|
+
// Windows 会忽略 mode,实际保护来自用户目录 ACL;POSIX 下为 0600。
|
|
50
|
+
writeFileSync(tmp, `${grant}\n`, { encoding: "utf8", mode: 0o600 });
|
|
51
|
+
renameSync(tmp, target); // 原子替换,避免读到半写文件
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// 落盘失败不能让签发失败:内存里的授权码在本次进程生命周期内仍然可用。
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function removePersistedGrant(sessionId) {
|
|
58
|
+
try {
|
|
59
|
+
rmSync(grantFilePath(sessionId), { force: true });
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* 删除失败不影响内存态 */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** 内存优先;内存没有则从磁盘恢复,避免重启后重新随机发码。 */
|
|
66
|
+
function expectedGrant(sessionId) {
|
|
67
|
+
const inMemory = grantsBySession.get(sessionId);
|
|
68
|
+
if (inMemory !== undefined)
|
|
69
|
+
return inMemory;
|
|
70
|
+
const persisted = readPersistedGrant(sessionId);
|
|
71
|
+
if (persisted !== undefined)
|
|
72
|
+
grantsBySession.set(sessionId, persisted);
|
|
73
|
+
return persisted;
|
|
74
|
+
}
|
|
3
75
|
export function issueAgentCapabilityGrant(sessionId) {
|
|
4
76
|
if (!sessionId)
|
|
5
77
|
throw new Error("sessionId is required for an agent capability grant");
|
|
6
|
-
const existing =
|
|
78
|
+
const existing = expectedGrant(sessionId);
|
|
7
79
|
if (existing !== undefined)
|
|
8
80
|
return existing;
|
|
9
81
|
const grant = randomBytes(32).toString("base64url");
|
|
10
82
|
grantsBySession.set(sessionId, grant);
|
|
83
|
+
persistGrant(sessionId, grant);
|
|
11
84
|
return grant;
|
|
12
85
|
}
|
|
13
86
|
export function validateAgentCapabilityGrant(sessionId, candidate) {
|
|
14
87
|
if (!sessionId || typeof candidate !== "string" || candidate.length === 0)
|
|
15
88
|
return false;
|
|
16
|
-
const expected =
|
|
89
|
+
const expected = expectedGrant(sessionId);
|
|
17
90
|
if (expected === undefined)
|
|
18
|
-
return false;
|
|
91
|
+
return false; // 查不到一律拒绝(fail closed)
|
|
19
92
|
const expectedBytes = Buffer.from(expected, "utf8");
|
|
20
93
|
const candidateBytes = Buffer.from(candidate, "utf8");
|
|
21
94
|
return expectedBytes.length === candidateBytes.length
|
|
22
95
|
&& timingSafeEqual(expectedBytes, candidateBytes);
|
|
23
96
|
}
|
|
97
|
+
/** 真正作废某个会话的授权(内存 + 磁盘)。 */
|
|
98
|
+
export function revokeAgentCapabilityGrant(sessionId) {
|
|
99
|
+
grantsBySession.delete(sessionId);
|
|
100
|
+
removePersistedGrant(sessionId);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 只清内存缓存,**不**删磁盘授权。
|
|
104
|
+
* 调用点:resetState()(进程首次启动)与测试清理。
|
|
105
|
+
* 若在这里同时删盘,则每次重启都会换码 —— 那正是本模块要修掉的故障。
|
|
106
|
+
*/
|
|
24
107
|
export function clearAgentCapabilityGrants() {
|
|
25
108
|
grantsBySession.clear();
|
|
26
109
|
}
|
|
@@ -128,7 +128,21 @@ function formatPostTextElement(el) {
|
|
|
128
128
|
if (el.tag === "p" || el.tag === "text") {
|
|
129
129
|
return t;
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
// 图片元素由 getPostImageKey 单独处理(可能带 image_key/file_key),此处不重复输出。
|
|
132
|
+
if (el.tag === "img" || getPostImageKey(el))
|
|
133
|
+
return "";
|
|
134
|
+
// 兜底:飞书可能新增富文本元素类型,而历史上曾因入口重写而整类元素被静默丢弃
|
|
135
|
+
// (见 tag="a" 丢失链接)。未知 tag 不再静默丢弃,尽量透传可读内容并留 warn 日志,
|
|
136
|
+
// 便于发现新增类型后补齐专门处理。
|
|
137
|
+
const tag = typeof el.tag === "string" ? el.tag : "(no tag)";
|
|
138
|
+
const href = typeof el.href === "string" ? el.href.trim() : "";
|
|
139
|
+
const userName = typeof el.user_name === "string" ? el.user_name.trim() : "";
|
|
140
|
+
const fallback = t && href && t !== href
|
|
141
|
+
? `[${t}](${href})`
|
|
142
|
+
: t || href || (userName ? `@${userName}` : "");
|
|
143
|
+
console.warn(`[${ts()}] [POST] 未知富文本元素 tag=${tag},` +
|
|
144
|
+
(fallback ? `已降级透传:${fallback}` : "无可透传内容,已丢弃(请补充分支)"));
|
|
145
|
+
return fallback;
|
|
132
146
|
}
|
|
133
147
|
function getPostImageKey(el) {
|
|
134
148
|
const imageKey = el.image_key;
|