evolcore 0.0.4 → 0.0.5

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/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
4
4
  [`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
5
5
 
6
+ ## 0.0.5 (2026-07-31)
7
+
8
+ ### 消息准入
9
+
10
+ - 群聊准入统一为开放、联系人、Owner 三种行为,移除额外的渠道白名单配置。
11
+ - 访问策略的修改权限收归 Agent Owner。
12
+ - 完善联系人准入、拉黑与 Owner 权限边界的判定。
13
+ - Connect 群组入口展示实时群组数量。
14
+
15
+ ### 运行可靠性
16
+
17
+ - 修复旧版本遗留的 AUN 会话目录身份恢复:可唯一判定身份时安全迁移,无法判定时保持原样并告警。
18
+ - 修正 Linux 容器内进程启动时间与运行时长的计算偏差。
19
+
20
+ ### 初始化体验
21
+
22
+ - 优化初始化向导的目录输入,支持家目录简写、错误重试与显式跳过。
23
+ - 未完成初始化时自动运行向导,完成后继续启动服务。
24
+ - 未配置 self-agent 时按实际可用入口给出创建提示。
25
+
6
26
  ## 0.0.4 (2026-07-30)
7
27
 
8
28
  ### 联系人身份与消息准入
@@ -13,9 +33,8 @@
13
33
  - 新增联系人请求服务,申请与状态流转改为原子操作,避免并发请求产生不一致。
14
34
  - 限制单实例和进程级待绑定请求数量,并限制无效绑定码的连续尝试次数。
15
35
  - 拒绝对不存在的联系人执行身份绑定和状态变更。
16
- - 群聊准入按 `access.policyMode` 分级:`open` 全面开放,`contacts` AUN 群聊默认开放、
17
- 非 AUN 群聊仍需命中 `allowedGroupChannels`,`owners-only` 一律拒绝。缺少 `access` 配置的
18
- 既有 Agent 走 `open` 兼容视图,升级后群聊不会因白名单为空被拒。
36
+ - 群聊准入按 `access.policyMode` 分级:`open` 对所有已配置渠道开放,`contacts` Agent 已加入并接收到的
37
+ 群聊视为授权,`owners` Owner 消息开放。
19
38
 
20
39
  ### 菜单与授权
21
40
 
@@ -29,6 +29,22 @@ import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
29
29
  import { WEB_CLI_BIN, WEB_PACKAGE_LATEST, WEB_PACKAGE_NAME } from '../product.js';
30
30
  import { rotateStdoutLog } from '../utils/log-writer.js';
31
31
  const execFileAsync = promisify(execFile);
32
+ function printNoSelfAgentHints(options) {
33
+ const { daemonConfig, ecwebStarted, skipped } = options;
34
+ console.log('\nℹ 未配置任何 self-agent,Control Plane 已启动。');
35
+ console.log(' 命令行:ec agent new <aid>.agentid.pub');
36
+ if (daemonConfig.aid && (daemonConfig.owners?.length ?? 0) > 0) {
37
+ console.log(' Evol App:可通过进程级菜单创建 agent');
38
+ }
39
+ if (ecwebStarted) {
40
+ console.log(' ECWeb:可通过控制台创建 agent');
41
+ }
42
+ if (skipped.length > 0) {
43
+ console.log('跳过的目录:');
44
+ for (const skippedAgent of skipped)
45
+ console.log(` - ${skippedAgent.dirName}: ${skippedAgent.reason}`);
46
+ }
47
+ }
32
48
  async function probeDaemon(socketPath, timeoutMs = 1000) {
33
49
  const response = await ipcQuery(socketPath, { type: 'ping' }, timeoutMs);
34
50
  if (response?.pong !== true || !Number.isInteger(response.pid) || response.pid <= 0)
@@ -196,8 +212,16 @@ export async function cmdStart(opts = {}) {
196
212
  }
197
213
  else {
198
214
  console.log('⚡ 未检测到初始化配置,自动启动初始化向导...\n');
199
- await cmdInit();
200
- return;
215
+ // cmdStart 直接调用 cmdInit,不会经过 CLI 的 `init` 分发分支;
216
+ // 这里需显式抑制 AUN SDK 的常规 keystore 日志。
217
+ const { suppressSdkLogs } = await import('../aun/aid/index.js');
218
+ suppressSdkLogs();
219
+ await cmdInit({ invokedByStart: true });
220
+ if (!loadDefaults()) {
221
+ console.log('⚠ 初始化未完成,未启动 EvolCore。');
222
+ return;
223
+ }
224
+ console.log('\n初始化完成,开始启动 EvolCore ....');
201
225
  }
202
226
  }
203
227
  // 控制 AID 门禁:缺 aid 且交互式 → 只补全控制 AID + owners(不重走 baseagent 向导)。
@@ -236,19 +260,12 @@ export async function cmdStart(opts = {}) {
236
260
  }
237
261
  // 检查至少有一个 self-agent
238
262
  const { agents, skipped } = loadAllAgents();
263
+ const isControlAidBootstrap = opts.bindBootstrap && !!daemonCfgStart.aid;
264
+ const shouldPrintNoSelfAgentHints = agents.length === 0 && !isControlAidBootstrap;
239
265
  if (agents.length === 0) {
240
- if (opts.bindBootstrap && daemonCfgStart.aid) {
266
+ if (isControlAidBootstrap) {
241
267
  console.log('ℹ 未配置任何 self-agent,绑定 bootstrap 将仅启动控制 AID');
242
268
  }
243
- else {
244
- console.log('ℹ 未配置任何 self-agent,将仅启动 Control Plane。');
245
- console.log(' 可通过控制 AID 远程创建 agent,或稍后运行 ec agent new <aid>.agentid.pub');
246
- if (skipped.length > 0) {
247
- console.log(`跳过的目录:`);
248
- for (const s of skipped)
249
- console.log(` - ${s.dirName}: ${s.reason}`);
250
- }
251
- }
252
269
  }
253
270
  // 检查 instance 目录中的进程状态
254
271
  const status = scanInstances();
@@ -323,10 +340,22 @@ export async function cmdStart(opts = {}) {
323
340
  console.log(` Logs: ${p.logs}/`);
324
341
  console.log(`⏱ ready in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
325
342
  if (!ecwebStartedBeforeDaemon) {
326
- startEcwebIfEnabled(p).catch((err) => {
343
+ startEcwebIfEnabled(p)
344
+ .then((ecwebStarted) => {
345
+ if (shouldPrintNoSelfAgentHints) {
346
+ printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted, skipped });
347
+ }
348
+ })
349
+ .catch((err) => {
327
350
  console.error(`⚠ ECWeb 启动检查失败: ${err instanceof Error ? err.message : String(err)}`);
351
+ if (shouldPrintNoSelfAgentHints) {
352
+ printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: false, skipped });
353
+ }
328
354
  });
329
355
  }
356
+ else if (shouldPrintNoSelfAgentHints) {
357
+ printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: true, skipped });
358
+ }
330
359
  }, 500);
331
360
  let forwardingShutdown = false;
332
361
  const forwardSignal = (signal) => {
@@ -426,8 +455,10 @@ export async function cmdStart(opts = {}) {
426
455
  }
427
456
  console.log(`⏱ done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
428
457
  // ECWeb 自动后台启动
429
- if (!ecwebStartedBeforeDaemon)
430
- await startEcwebIfEnabled(p);
458
+ const ecwebStarted = ecwebStartedBeforeDaemon || await startEcwebIfEnabled(p);
459
+ if (shouldPrintNoSelfAgentHints) {
460
+ printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted, skipped });
461
+ }
431
462
  return;
432
463
  }
433
464
  // 超时
package/dist/cli/init.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from 'fs';
2
+ import os from 'os';
2
3
  import path from 'path';
3
4
  import readline from 'readline';
4
5
  import { resolvePaths, ensureDataDirs } from '../paths.js';
@@ -13,6 +14,15 @@ import { WEB_CLI_BIN, WEB_PACKAGE_LATEST } from '../product.js';
13
14
  function ask(rl, question) {
14
15
  return new Promise(resolve => rl.question(question, resolve));
15
16
  }
17
+ /** 展开用户在交互式输入中使用的 home 目录简写(shell 不会替 readline 自动做这件事)。 */
18
+ export function expandLeadingTildePath(value, homeDir = os.homedir()) {
19
+ if (value === '~')
20
+ return homeDir;
21
+ if (value.startsWith('~/') || value.startsWith('~\\')) {
22
+ return path.join(homeDir, value.slice(2));
23
+ }
24
+ return value;
25
+ }
16
26
  const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
17
27
  function isBaseagentAvailable(baseagent) {
18
28
  if (baseagent === 'codex')
@@ -167,7 +177,7 @@ export async function cmdInit(options) {
167
177
  await runInteractive();
168
178
  }
169
179
  // ── 共享 tail(单一出口):提示创建 agent + 生成控制 AID ──
170
- await initTail();
180
+ await initTail({ invokedByStart: options?.invokedByStart });
171
181
  // ── 内部函数 ──
172
182
  async function runInteractive() {
173
183
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -194,29 +204,36 @@ export async function cmdInit(options) {
194
204
  }
195
205
  async function askProjectsDefaultPath() {
196
206
  const defaultDir = path.join(defaultProjectsRoot(p.root), 'default');
197
- const input = (await ask(rl, `项目默认目录 [${defaultDir}]: `)).trim();
198
- const resolved = input || defaultDir;
199
- if (!path.isAbsolute(resolved)) {
200
- console.log(' ⚠ 需要绝对路径,已跳过');
201
- return undefined;
202
- }
203
- if (!fs.existsSync(resolved)) {
204
- const create = (await ask(rl, ` 目录不存在,是否创建?[Y/n]: `)).trim().toLowerCase();
205
- if (create === '' || create === 'y' || create === 'yes') {
206
- try {
207
- fs.mkdirSync(resolved, { recursive: true });
207
+ while (true) {
208
+ const input = (await ask(rl, `项目默认目录 [${defaultDir}](输入 skip 跳过): `)).trim();
209
+ if (input.toLowerCase() === 'skip') {
210
+ console.log(' 已跳过项目默认目录配置');
211
+ return undefined;
212
+ }
213
+ const resolved = expandLeadingTildePath(input || defaultDir);
214
+ if (!path.isAbsolute(resolved)) {
215
+ console.log(' 请输入绝对路径或回车使用默认值;输入 skip 可跳过');
216
+ continue;
217
+ }
218
+ if (!fs.existsSync(resolved)) {
219
+ const create = (await ask(rl, ` 目录不存在,是否创建?[Y/n]: `)).trim().toLowerCase();
220
+ if (create === '' || create === 'y' || create === 'yes') {
221
+ try {
222
+ fs.mkdirSync(resolved, { recursive: true });
223
+ }
224
+ catch (e) {
225
+ console.log(` ⚠ 创建目录失败: ${e?.message || e}`);
226
+ continue;
227
+ }
228
+ console.log(` ✓ 已创建 ${resolved}`);
208
229
  }
209
- catch (e) {
210
- console.log(` 创建目录失败: ${e?.message || e}`);
211
- return undefined;
230
+ else {
231
+ console.log(' 未创建目录,请重新输入路径;输入 skip 可跳过');
232
+ continue;
212
233
  }
213
- console.log(` ✓ 已创建 ${resolved}`);
214
- }
215
- else {
216
- return undefined;
217
234
  }
235
+ return resolved;
218
236
  }
219
- return resolved;
220
237
  }
221
238
  try {
222
239
  if (defaultsExisted) {
@@ -249,10 +266,10 @@ export async function cmdInit(options) {
249
266
  }
250
267
  }
251
268
  /** 补全控制 AID + owners(可单独调用,不走 baseagent 向导)。 */
252
- export async function initTail() {
269
+ export async function initTail(options = {}) {
253
270
  // 提示创建 agent(两分支汇合后执行一次)
254
271
  const { agents } = loadAllAgents();
255
- if (agents.length === 0) {
272
+ if (agents.length === 0 && !options.invokedByStart) {
256
273
  console.log('\n提示:尚无 agent,运行以下命令创建:');
257
274
  console.log(' ec agent new <aid>.agentid.pub');
258
275
  }
@@ -368,6 +385,9 @@ export async function initTail() {
368
385
  }
369
386
  }
370
387
  }
388
+ // ec start 会接着启动 daemon,并在其 ready 后给出可用的 Agent 创建入口。
389
+ if (options.invokedByStart)
390
+ return;
371
391
  // 初始化完成总结
372
392
  console.log('\n✓ EvolCore 初始化完成');
373
393
  const finalCfg = loadDaemonConfig();
@@ -4,18 +4,6 @@ export function validAgentOwnerIds(agent) {
4
4
  .map(value => String(value || '').trim())
5
5
  .filter(isValidAid))];
6
6
  }
7
- export function configuredChannelKeys(selfAid, agent) {
8
- const keys = (agent?.channels ?? [])
9
- .filter(channel => channel && channel.enabled !== false)
10
- .map(channel => {
11
- const type = String(channel.type || '').trim().toLowerCase();
12
- const name = String(channel.name || '').trim();
13
- return type && name ? `${type}#${selfAid}#${name}` : '';
14
- })
15
- .filter(Boolean);
16
- keys.push(`aun#${selfAid}#main`);
17
- return [...new Set(keys)];
18
- }
19
7
  export function validateAccessPolicyDomain(selfAid, agent) {
20
8
  const access = agent.access;
21
9
  if (!access)
@@ -26,13 +14,5 @@ export function validateAccessPolicyDomain(selfAid, agent) {
26
14
  message: `Agent ${selfAid} cannot use ${access.policyMode} without an Owner`,
27
15
  };
28
16
  }
29
- const configured = new Set(configuredChannelKeys(selfAid, agent));
30
- const unknown = access.allowedGroupChannels.find(channelKey => !configured.has(channelKey));
31
- if (unknown) {
32
- return {
33
- code: 'unknown_group_access_channel',
34
- message: `Group access channel is not configured or enabled for ${selfAid}: ${unknown}`,
35
- };
36
- }
37
17
  return undefined;
38
18
  }
@@ -1,6 +1,6 @@
1
1
  import crypto from 'crypto';
2
2
  import { ConfigError, ConfigTarget, mutateConfig, read, validateConfig, } from './config-manager.js';
3
- import { configuredChannelKeys, validAgentOwnerIds, validateAccessPolicyDomain, } from './access-policy-domain.js';
3
+ import { validAgentOwnerIds, validateAccessPolicyDomain, } from './access-policy-domain.js';
4
4
  export class AccessPolicyError extends Error {
5
5
  code;
6
6
  data;
@@ -13,20 +13,13 @@ export class AccessPolicyError extends Error {
13
13
  }
14
14
  export const DEFAULT_ACCESS_POLICY = Object.freeze({
15
15
  policyMode: 'open',
16
- allowedGroupChannels: Object.freeze([]),
17
16
  requestLimit: Object.freeze({ maxTimes: 600, coolDown: 60 * 60 }),
18
17
  });
19
18
  export function listAgentOwners(selfAid) {
20
19
  const agent = read(ConfigTarget.Agent, { self: selfAid }, { cache: true });
21
20
  return validAgentOwnerIds(agent);
22
21
  }
23
- export function listConfiguredChannelKeys(selfAid) {
24
- const agent = read(ConfigTarget.Agent, { self: selfAid }, { cache: true });
25
- if (!agent)
26
- return [];
27
- return configuredChannelKeys(selfAid, agent);
28
- }
29
- /** Missing AgentConfig.access uses the compatibility view; malformed Agent config never does. */
22
+ /** Missing AgentConfig.access uses the default open policy; malformed Agent config never does. */
30
23
  export function readAccessPolicy(selfAid) {
31
24
  return readAccessPolicySnapshot(selfAid).policy;
32
25
  }
@@ -49,7 +42,7 @@ export function mutateAccessPolicy(input) {
49
42
  });
50
43
  }
51
44
  const next = input.update(clonePolicy(current.policy));
52
- assertAccessPolicyWrite(input, agent, current.policy, next);
45
+ assertAccessPolicyWrite(input, agent, next);
53
46
  agent.access = clonePolicy(next);
54
47
  return {
55
48
  policy: clonePolicy(next),
@@ -85,37 +78,20 @@ function accessPolicySnapshotFromAgent(selfAid, agent) {
85
78
  const policy = clonePolicy(access);
86
79
  return { policy, accessRevision: accessPolicyRevision(policy) };
87
80
  }
88
- function assertAccessPolicyWrite(input, agent, current, policy) {
81
+ function assertAccessPolicyWrite(input, agent, policy) {
89
82
  const owners = validAgentOwnerIds(agent);
90
83
  const normalizedActorAid = String(input.actorAid || '').trim();
91
84
  const isOwner = input.actorRole === 'owner' || owners.includes(normalizedActorAid);
92
- const isGroupChannelAdmin = input.actorRole === 'admin' && input.resource === 'access.allowedGroupChannels';
93
- if (!isOwner && !isGroupChannelAdmin) {
94
- if (input.resource === 'access.allowedGroupChannels') {
95
- throw new AccessPolicyError('access_policy_manager_required', 'Only an Agent Owner or Admin can change allowed group channels');
96
- }
85
+ if (!isOwner) {
97
86
  throw new AccessPolicyError('access_policy_owner_required', 'Only an Agent Owner can change access policy');
98
87
  }
99
- if (isGroupChannelAdmin) {
100
- if (current.policyMode !== policy.policyMode
101
- || current.requestLimit.maxTimes !== policy.requestLimit.maxTimes
102
- || current.requestLimit.coolDown !== policy.requestLimit.coolDown) {
103
- throw new AccessPolicyError('access_policy_owner_required', 'An Agent Admin may only change allowed group channels');
104
- }
105
- }
106
88
  if (policy.policyMode !== 'open' && owners.length === 0) {
107
89
  throw new AccessPolicyError('restricted_mode_without_owner', `Agent ${input.selfAid} cannot use ${policy.policyMode} without an Owner`);
108
90
  }
109
- const configured = new Set(configuredChannelKeys(input.selfAid, agent));
110
- const unknown = policy.allowedGroupChannels.find(channelKey => !configured.has(channelKey));
111
- if (unknown) {
112
- throw new AccessPolicyError('unknown_group_access_channel', `Group access channel is not configured or enabled for ${input.selfAid}: ${unknown}`);
113
- }
114
91
  }
115
92
  function clonePolicy(policy) {
116
93
  return {
117
94
  policyMode: policy.policyMode,
118
- allowedGroupChannels: [...policy.allowedGroupChannels],
119
95
  requestLimit: { ...policy.requestLimit },
120
96
  };
121
97
  }
@@ -397,7 +397,7 @@ const OPERATIONS = [
397
397
  category: 'write-agent',
398
398
  dangerous: false,
399
399
  defaultScopes: ['agent'],
400
- description: 'Manage contacts, contact requests, and allowed group channels',
400
+ description: 'Manage contacts and contact requests',
401
401
  sources: ['menu', 'ecweb', 'control'],
402
402
  },
403
403
  {
@@ -1,6 +1,6 @@
1
- import { roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
1
+ import { roleMenuQuery, roleMenuGroupCount, roleMenuUpdate, } from './role-menu.js';
2
2
  import { getContactSnapshot, resolveContactView } from '../../config/contact-book.js';
3
- import { listConfiguredChannelKeys, mutateAccessPolicy, readAccessPolicySnapshot, } from '../../config/access-policy.js';
3
+ import { mutateAccessPolicy, readAccessPolicySnapshot, } from '../../config/access-policy.js';
4
4
  import { expirePendingContactRequests, mutateContactWithOperation, reviewContactRequest, } from '../../config/contact-request-service.js';
5
5
  import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
6
6
  /**
@@ -13,8 +13,8 @@ import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
13
13
  * 群组不落 contact.json(contact book 只接受个人 AID,group 会被拒绝)。
14
14
  * - 联系人与申请(view=contacts / contact,add / block / unblock / review)→ 直接读写
15
15
  * Contact Book v3;contact.json 是当前状态和 pending 版本的唯一事实来源。
16
- * - 访问设置(view=access / option=allowedGroupChannels / resource=<access field>)→
17
- * 查询策略、列举群聊渠道候选项并逐字段写入 Agent config.json 的 access 字段。
16
+ * - 访问设置(view=access / resource=<access field>)→
17
+ * 查询和写入 Agent config.json 的 access 字段。
18
18
  */
19
19
  const MENU_NAME = 'connect';
20
20
  const DEFAULT_PAGE_SIZE = 50;
@@ -31,8 +31,6 @@ export function connectMenuOperation(kind, args, value) {
31
31
  if (kind === 'action')
32
32
  return 'connect.write';
33
33
  const resource = String(args?.resource ?? '');
34
- if (resource === 'allowedGroupChannels')
35
- return 'connect.write';
36
34
  if (resource === 'policyMode' || resource === 'requestLimit') {
37
35
  return 'connect.access.write';
38
36
  }
@@ -55,7 +53,7 @@ export async function handleConnectMenu(req, context) {
55
53
  }
56
54
  try {
57
55
  if (req.subtype === 'options')
58
- return await handleOptions(self, req.args);
56
+ return await handleOptions(self, req.args, context);
59
57
  if (req.subtype === 'query')
60
58
  return await handleQuery(req, self, context);
61
59
  if (req.subtype === 'update')
@@ -69,26 +67,24 @@ export async function handleConnectMenu(req, context) {
69
67
  return fail('INVALID_ARGUMENT', `Unknown subtype: ${req.subtype}`);
70
68
  }
71
69
  // ── Options ──
72
- async function handleOptions(self, args) {
70
+ async function handleOptions(self, args, context) {
73
71
  const option = String(args?.option ?? '').trim();
74
72
  if (option) {
75
- if (option !== 'allowedGroupChannels') {
76
- return fail('NOT_SUPPORTED', `Unknown connect option: ${option}`);
77
- }
78
- const selected = new Set(readAccessPolicySnapshot(self).policy.allowedGroupChannels);
79
- return ok({
80
- options: listConfiguredChannelKeys(self).map(channelKey => ({
81
- option: channelKey,
82
- label: channelKey,
83
- selected: selected.has(channelKey),
84
- })),
85
- });
73
+ return fail('NOT_SUPPORTED', `Unknown connect option: ${option}`);
86
74
  }
87
75
  await expirePendingContactRequests(self);
88
76
  const snapshot = getContactSnapshot(self);
89
77
  const contactCount = snapshot.contacts.size;
90
78
  const blockedCount = snapshot.blockedIndex.size;
91
79
  const pendingCount = [...snapshot.contacts.values()].filter(entry => entry.status === 'pending').length;
80
+ let groupsDescription = '查看所在群组(数量暂不可用)';
81
+ try {
82
+ const groupCount = await roleMenuGroupCount(context);
83
+ groupsDescription = `查看所在群组(${groupCount} 个)`;
84
+ }
85
+ catch {
86
+ // Keep the root menu available when AUN group directory is disabled or unavailable.
87
+ }
92
88
  return ok({
93
89
  options: [
94
90
  {
@@ -99,12 +95,12 @@ async function handleOptions(self, args) {
99
95
  {
100
96
  option: 'groups',
101
97
  label: '群组列表',
102
- description: '查看所在群组',
98
+ description: groupsDescription,
103
99
  },
104
100
  {
105
101
  option: 'access',
106
102
  label: '访问设置',
107
- description: '设置私聊准入模式与允许群聊的渠道',
103
+ description: '设置消息准入模式和准入控制消息限额',
108
104
  },
109
105
  ],
110
106
  });
@@ -268,15 +264,11 @@ async function handleAction(req, self, context) {
268
264
  }
269
265
  function isAccessUpdateResource(resource) {
270
266
  return resource === 'policyMode'
271
- || resource === 'allowedGroupChannels'
272
267
  || resource === 'requestLimit';
273
268
  }
274
269
  function updateAccessResource(req, context, resource) {
275
270
  const args = req.args ?? {};
276
- if (resource === 'allowedGroupChannels')
277
- requireAdminContext(context);
278
- else
279
- requireOwnerContext(context);
271
+ requireOwnerContext(context);
280
272
  if (req.value === undefined || !req.value.trim()) {
281
273
  return fail('INVALID_ARGUMENT', 'value is required');
282
274
  }
@@ -284,15 +276,8 @@ function updateAccessResource(req, context, resource) {
284
276
  const expectedAccessRevision = requireAccessRevision(args);
285
277
  const decoded = decodeAccessUpdateValue(req.value);
286
278
  if (resource === 'policyMode') {
287
- if (typeof decoded !== 'string' || !['open', 'contacts', 'owners-only'].includes(decoded)) {
288
- return fail('INVALID_ARGUMENT', 'policyMode must be open, contacts, or owners-only');
289
- }
290
- }
291
- else if (resource === 'allowedGroupChannels') {
292
- if (!Array.isArray(decoded)
293
- || !decoded.every(value => typeof value === 'string' && value.length > 0)
294
- || new Set(decoded).size !== decoded.length) {
295
- return fail('INVALID_ARGUMENT', 'allowedGroupChannels must be a JSON string array');
279
+ if (typeof decoded !== 'string' || !['open', 'contacts', 'owners'].includes(decoded)) {
280
+ return fail('INVALID_ARGUMENT', 'policyMode must be open, contacts, or owners');
296
281
  }
297
282
  }
298
283
  else {
@@ -452,16 +437,12 @@ function errorCode(error) {
452
437
  return 'INTERNAL_ERROR';
453
438
  if (code === 'access_policy_owner_required')
454
439
  return 'PERMISSION_DENIED';
455
- if (code === 'access_policy_manager_required')
456
- return 'PERMISSION_DENIED';
457
440
  if (code === 'access_policy_revision_conflict')
458
441
  return 'CONFLICT';
459
442
  if (code === 'access_policy_temporarily_unavailable')
460
443
  return 'TEMPORARILY_UNAVAILABLE';
461
444
  if (code === 'restricted_mode_without_owner')
462
445
  return 'NOT_ALLOWED';
463
- if (code === 'unknown_group_access_channel')
464
- return 'INVALID_ARGUMENT';
465
446
  if (code === 'invalid_access_policy')
466
447
  return 'INTERNAL_ERROR';
467
448
  return typeof code === 'string' ? code : 'INTERNAL_ERROR';
@@ -538,9 +519,6 @@ function accessPolicyWithUpdate(current, resource, decoded) {
538
519
  if (resource === 'policyMode') {
539
520
  return { ...current, policyMode: decoded };
540
521
  }
541
- if (resource === 'allowedGroupChannels') {
542
- return { ...current, allowedGroupChannels: [...decoded] };
543
- }
544
522
  const requestLimit = decoded;
545
523
  return {
546
524
  ...current,
@@ -156,6 +156,22 @@ export async function roleMenuQuery(context, args) {
156
156
  view: view || null,
157
157
  });
158
158
  }
159
+ /**
160
+ * Read the current joined-group count without materializing the full group list.
161
+ * Used by Connect's root options description; failures are handled by the caller
162
+ * so a transient AUN directory outage does not hide the rest of the menu.
163
+ */
164
+ export async function roleMenuGroupCount(context) {
165
+ assertRequest(context, { self: context.self });
166
+ assertAunDirectory(context);
167
+ const result = await roleDirectory(context).listGroups({ from: context.self, size: 1 });
168
+ if (!result.ok) {
169
+ throw roleError('TEMPORARILY_UNAVAILABLE', result.error || 'Unable to list joined groups', context.self, {
170
+ kind: 'target_source_unavailable', targetType: 'group',
171
+ });
172
+ }
173
+ return Math.max(Number(result.total) || 0, result.items.length);
174
+ }
159
175
  export async function roleMenuUpdate(context, args, rawValue) {
160
176
  assertRequest(context, args);
161
177
  const value = parseUpdateValue(rawValue, context.self);
@@ -44,6 +44,23 @@ export function evaluateInboundAdmissionPreflight(input) {
44
44
  }
45
45
  }
46
46
  }
47
+ else {
48
+ // Group admission is based on group membership in `contacts` mode, but
49
+ // owners mode still needs to identify an Owner sender. Resolve only the
50
+ // canonical identity needed for that distinction; group messages do not
51
+ // use contact status or private-channel blocking rules.
52
+ try {
53
+ const principal = resolvePrincipal(selfAid, channelType, actorId, input.channelKey);
54
+ if (principal.principalId) {
55
+ context.primaryId = principal.principalId;
56
+ context.isOwner = resolveContactView(selfAid, principal.principalId).isOwner;
57
+ }
58
+ }
59
+ catch {
60
+ // Open/contacts group admission remains membership-based even when a
61
+ // native identity cannot be resolved through the contact book.
62
+ }
63
+ }
47
64
  try {
48
65
  context.policy = readAccessPolicy(selfAid);
49
66
  }
@@ -62,26 +79,19 @@ export function evaluateOrdinaryInboundAdmission(context) {
62
79
  if (!policy)
63
80
  return { allow: false, reason: 'invalid_access_policy', context };
64
81
  if (context.chatType === 'group') {
65
- if (policy.policyMode === 'owners-only') {
66
- return { allow: false, reason: 'owners_only', context };
67
- }
68
- // `open` is genuinely open: group traffic is not gated by the optional
69
- // channel allow-list. In `contacts`, AUN groups are currently open once
70
- // the agent is receiving that group; non-AUN channels remain explicit.
71
- if (policy.policyMode === 'open'
72
- || (policy.policyMode === 'contacts' && context.channelType === 'aun')) {
82
+ if (context.isOwner)
83
+ return { allow: true, reason: 'owner', context };
84
+ if (policy.policyMode === 'open' || policy.policyMode === 'contacts') {
73
85
  return { allow: true, reason: 'group_channel_allowed', context };
74
86
  }
75
- return policy.allowedGroupChannels.includes(context.channelKey)
76
- ? { allow: true, reason: 'group_channel_allowed', context }
77
- : { allow: false, reason: 'group_channel_denied', context };
87
+ return { allow: false, reason: 'owners', context };
78
88
  }
79
89
  if (context.isOwner)
80
90
  return { allow: true, reason: 'owner', context };
81
91
  if (policy.policyMode === 'open')
82
92
  return { allow: true, reason: 'open', context };
83
- if (policy.policyMode === 'owners-only')
84
- return { allow: false, reason: 'owners_only', context };
93
+ if (policy.policyMode === 'owners')
94
+ return { allow: false, reason: 'owners', context };
85
95
  if (context.primaryId && isAcceptedContact(context.selfAid, context.primaryId)) {
86
96
  return { allow: true, reason: 'contact', context };
87
97
  }
@@ -10,15 +10,25 @@ function encodeSegment(s) {
10
10
  function decodeSegment(s) {
11
11
  return s.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
12
12
  }
13
+ function resolveAunSelfAID(selfAID, channelKey) {
14
+ const direct = selfAID?.trim();
15
+ if (direct && direct !== '_unknown')
16
+ return direct;
17
+ const parts = channelKey?.split('#');
18
+ const inferred = parts?.length === 3 && parts[0] === 'aun' ? parts[1]?.trim() : undefined;
19
+ if (inferred && inferred !== '_unknown')
20
+ return inferred;
21
+ throw new Error('[SessionFsStore] AUN chat directory requires selfAID or an aun channel key.');
22
+ }
13
23
  /**
14
24
  * 计算 chat 目录的完整路径。
15
- * - aun: sessionsDir/aun/<urlEncode(selfAID|'_unknown')>/<urlEncode(channelId)>/
25
+ * - aun: sessionsDir/aun/<urlEncode(selfAID)>/<urlEncode(channelId)>/
16
26
  * - wecom: sessionsDir/wecom/<urlEncode(channelKey)>/<urlEncode(channelId)>/
17
27
  * - 其它: sessionsDir/<channelType>/<urlEncode(channelId)>/
18
28
  */
19
29
  export function chatDirPath(sessionsDir, channelType, channelId, selfAID, channelKey) {
20
30
  if (channelType === 'aun') {
21
- return path.join(sessionsDir, channelType, encodeSegment(selfAID || '_unknown'), encodeSegment(channelId));
31
+ return path.join(sessionsDir, channelType, encodeSegment(resolveAunSelfAID(selfAID, channelKey)), encodeSegment(channelId));
22
32
  }
23
33
  if (channelType === 'wecom' && channelKey?.startsWith('wecom#')) {
24
34
  return path.join(sessionsDir, channelType, encodeSegment(channelKey), encodeSegment(channelId));
@@ -47,9 +57,8 @@ export function formatTimestamp(epochMs) {
47
57
  const ss = String(d.getSeconds()).padStart(2, '0');
48
58
  return `${yyyy}-${mo}-${dd} ${hh}:${mi}:${ss}`;
49
59
  }
50
- export function atomicWriteJson(filePath, data) {
60
+ function atomicWrite(filePath, content) {
51
61
  const tmpPath = filePath + '.tmp';
52
- const content = JSON.stringify(data, null, 2) + '\n';
53
62
  const fd = fs.openSync(tmpPath, 'w');
54
63
  fs.writeSync(fd, content);
55
64
  fs.fsyncSync(fd);
@@ -63,6 +72,12 @@ export function atomicWriteJson(filePath, data) {
63
72
  }
64
73
  fs.renameSync(tmpPath, filePath);
65
74
  }
75
+ export function atomicWriteJson(filePath, data) {
76
+ atomicWrite(filePath, JSON.stringify(data, null, 2) + '\n');
77
+ }
78
+ export function atomicWriteText(filePath, content) {
79
+ atomicWrite(filePath, content);
80
+ }
66
81
  export function appendJsonl(filePath, record) {
67
82
  const line = JSON.stringify(record) + '\n';
68
83
  const fd = fs.openSync(filePath, 'a');
@@ -1,7 +1,7 @@
1
1
  import { ensureDir } from '../../utils/atomic-write.js';
2
2
  import { logger } from '../../utils/logger.js';
3
3
  import { encodePath } from '../../utils/cross-platform.js';
4
- import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, appendJsonl, readJsonFile, readLastJsonlLine, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
4
+ import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, readJsonFile, readLastJsonlLine, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
5
5
  import { sessionToFile, fileToSession } from './session-mapper.js';
6
6
  import { formatSessionKey, DEFAULT_THREAD_ID } from './session-key.js';
7
7
  import { tryParseChannelKey } from '../channel-loader.js';
@@ -39,6 +39,7 @@ export class SessionManager {
39
39
  this.eventBus = eventBus;
40
40
  this.identityResolver = identityResolver;
41
41
  this.migrateChannelKeyFormat();
42
+ this.migrateUnknownAunDirs();
42
43
  this.migrateWecomChannelDirs();
43
44
  }
44
45
  setIdentityResolver(resolver) {
@@ -1662,4 +1663,209 @@ export class SessionManager {
1662
1663
  logger.info(`[SessionManager] Migrated ${migrated} WeCom session director${migrated === 1 ? 'y' : 'ies'} to channelKey isolation`);
1663
1664
  }
1664
1665
  }
1666
+ /**
1667
+ * Recover legacy AUN chats written beneath `_unknown` before selfAID was
1668
+ * persisted. A channel key or persisted selfAID is required to make the
1669
+ * migration unambiguous; all other directories remain untouched.
1670
+ */
1671
+ migrateUnknownAunDirs() {
1672
+ const legacyDirs = scanChatDirs(this.sessionsDir)
1673
+ .filter(entry => entry.channelType === 'aun' && entry.selfAID === '_unknown');
1674
+ let migrated = 0;
1675
+ for (const entry of legacyDirs) {
1676
+ const selfAIDs = this.findAunSelfAIDCandidates(entry.dirPath);
1677
+ if (selfAIDs.size !== 1) {
1678
+ if (selfAIDs.size > 1) {
1679
+ logger.error(`[SessionManager] Cannot auto-migrate mixed unknown AUN session directory: ${entry.dirPath}`);
1680
+ }
1681
+ continue;
1682
+ }
1683
+ const selfAID = [...selfAIDs][0];
1684
+ const target = chatDirPath(this.sessionsDir, 'aun', entry.channelId, selfAID);
1685
+ try {
1686
+ this.mergeUnknownAunDirectory(entry.dirPath, target, selfAID);
1687
+ migrated++;
1688
+ }
1689
+ catch (error) {
1690
+ logger.error(`[SessionManager] Failed to migrate unknown AUN session directory ${entry.dirPath}: ${String(error)}`);
1691
+ }
1692
+ }
1693
+ if (migrated > 0) {
1694
+ logger.info(`[SessionManager] Migrated ${migrated} unknown AUN session director${migrated === 1 ? 'y' : 'ies'} to selfAID isolation`);
1695
+ }
1696
+ }
1697
+ findAunSelfAIDCandidates(chatDir) {
1698
+ const candidates = new Set();
1699
+ const visitRecord = (record) => {
1700
+ if (!record || typeof record !== 'object')
1701
+ return;
1702
+ const session = record;
1703
+ const direct = typeof session.selfAID === 'string' ? session.selfAID.trim() : '';
1704
+ if (direct && direct !== '_unknown')
1705
+ candidates.add(direct);
1706
+ const channel = typeof session.channel === 'string' ? tryParseChannelKey(session.channel) : null;
1707
+ if (channel?.type === 'aun' && channel.selfAID !== '_unknown')
1708
+ candidates.add(channel.selfAID);
1709
+ };
1710
+ const visitJsonl = (filePath) => {
1711
+ for (const record of readAllJsonlLines(filePath))
1712
+ visitRecord(record);
1713
+ };
1714
+ visitRecord(readJsonFile(path.join(chatDir, 'active.json')));
1715
+ for (const metaFile of scanMetaFiles(chatDir))
1716
+ visitJsonl(path.join(chatDir, metaFile));
1717
+ const threadDir = path.join(chatDir, '_threads');
1718
+ for (const metaFile of scanMetaFiles(threadDir))
1719
+ visitJsonl(path.join(threadDir, metaFile));
1720
+ return candidates;
1721
+ }
1722
+ mergeUnknownAunDirectory(sourceDir, targetDir, selfAID) {
1723
+ if (sourceDir === targetDir)
1724
+ return;
1725
+ fs.mkdirSync(targetDir, { recursive: true });
1726
+ this.mergeUnknownAunDirectoryContents(sourceDir, targetDir, selfAID);
1727
+ fs.rmdirSync(sourceDir);
1728
+ }
1729
+ mergeUnknownAunDirectoryContents(sourceDir, targetDir, selfAID) {
1730
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
1731
+ const sourcePath = path.join(sourceDir, entry.name);
1732
+ const targetPath = path.join(targetDir, entry.name);
1733
+ if (entry.isDirectory()) {
1734
+ fs.mkdirSync(targetPath, { recursive: true });
1735
+ this.mergeUnknownAunDirectoryContents(sourcePath, targetPath, selfAID);
1736
+ fs.rmdirSync(sourcePath);
1737
+ continue;
1738
+ }
1739
+ if (!entry.isFile())
1740
+ continue;
1741
+ if (entry.name === 'active.json') {
1742
+ this.mergeUnknownAunActiveFile(sourcePath, targetPath, selfAID);
1743
+ }
1744
+ else if (entry.name.endsWith('.jsonl')) {
1745
+ this.mergeUnknownAunJsonlFile(sourcePath, targetPath, selfAID);
1746
+ }
1747
+ else if (entry.name.endsWith('.json')) {
1748
+ this.mergeUnknownAunJsonFile(sourcePath, targetPath);
1749
+ }
1750
+ else if (!fs.existsSync(targetPath)) {
1751
+ fs.renameSync(sourcePath, targetPath);
1752
+ }
1753
+ else if (fs.readFileSync(sourcePath).equals(fs.readFileSync(targetPath))) {
1754
+ fs.unlinkSync(sourcePath);
1755
+ }
1756
+ else {
1757
+ fs.renameSync(sourcePath, this.uniqueLegacyFilePath(targetPath));
1758
+ }
1759
+ }
1760
+ }
1761
+ mergeUnknownAunActiveFile(sourcePath, targetPath, selfAID) {
1762
+ const source = this.normalizeAunSessionRecord(readJsonFile(sourcePath), selfAID);
1763
+ const target = this.normalizeAunSessionRecord(readJsonFile(targetPath), selfAID);
1764
+ const sourceUpdatedAt = typeof source?.updatedAt === 'number' ? source.updatedAt : 0;
1765
+ const targetUpdatedAt = typeof target?.updatedAt === 'number' ? target.updatedAt : 0;
1766
+ const selected = target && targetUpdatedAt >= sourceUpdatedAt ? target : source;
1767
+ if (selected)
1768
+ atomicWriteJson(targetPath, selected);
1769
+ fs.unlinkSync(sourcePath);
1770
+ }
1771
+ mergeUnknownAunJsonlFile(sourcePath, targetPath, selfAID) {
1772
+ const records = [
1773
+ ...this.readNormalizedAunJsonl(sourcePath, selfAID),
1774
+ ...this.readNormalizedAunJsonl(targetPath, selfAID),
1775
+ ];
1776
+ const seen = new Set();
1777
+ const unique = records.filter(record => {
1778
+ if (seen.has(record.key))
1779
+ return false;
1780
+ seen.add(record.key);
1781
+ return true;
1782
+ });
1783
+ unique.sort((left, right) => left.timestamp - right.timestamp || left.order - right.order);
1784
+ atomicWriteText(targetPath, unique.map(record => record.line).join('\n') + (unique.length ? '\n' : ''));
1785
+ fs.unlinkSync(sourcePath);
1786
+ }
1787
+ readNormalizedAunJsonl(filePath, selfAID) {
1788
+ let content;
1789
+ try {
1790
+ content = fs.readFileSync(filePath, 'utf-8');
1791
+ }
1792
+ catch (error) {
1793
+ if (error?.code === 'ENOENT')
1794
+ return [];
1795
+ throw error;
1796
+ }
1797
+ return content.split('\n').flatMap((rawLine, order) => {
1798
+ const line = rawLine.trim();
1799
+ if (!line)
1800
+ return [];
1801
+ try {
1802
+ const record = this.normalizeAunSessionRecord(JSON.parse(line), selfAID);
1803
+ const normalizedLine = JSON.stringify(record);
1804
+ return [{
1805
+ key: this.stableJson(record),
1806
+ line: normalizedLine,
1807
+ timestamp: this.recordTimestamp(record),
1808
+ order,
1809
+ }];
1810
+ }
1811
+ catch {
1812
+ return [{ key: `raw:${line}`, line, timestamp: Number.MAX_SAFE_INTEGER, order }];
1813
+ }
1814
+ });
1815
+ }
1816
+ normalizeAunSessionRecord(record, selfAID) {
1817
+ if (!record || typeof record !== 'object' || Array.isArray(record))
1818
+ return record;
1819
+ const result = { ...record };
1820
+ const isSessionRecord = 'channel' in result || 'channelType' in result || 'sessionKey' in result || 'selfAID' in result;
1821
+ if (!isSessionRecord)
1822
+ return result;
1823
+ if (!result.selfAID || result.selfAID === '_unknown')
1824
+ result.selfAID = selfAID;
1825
+ return result;
1826
+ }
1827
+ stableJson(value) {
1828
+ if (Array.isArray(value))
1829
+ return `[${value.map(item => this.stableJson(item)).join(',')}]`;
1830
+ if (value && typeof value === 'object') {
1831
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${this.stableJson(value[key])}`).join(',')}}`;
1832
+ }
1833
+ return JSON.stringify(value);
1834
+ }
1835
+ recordTimestamp(record) {
1836
+ if (!record || typeof record !== 'object')
1837
+ return Number.MAX_SAFE_INTEGER;
1838
+ const value = record;
1839
+ for (const key of ['ts', 'at', 'updatedAt', 'createdAt']) {
1840
+ if (typeof value[key] === 'number' && Number.isFinite(value[key]))
1841
+ return value[key];
1842
+ }
1843
+ return Number.MAX_SAFE_INTEGER;
1844
+ }
1845
+ mergeUnknownAunJsonFile(sourcePath, targetPath) {
1846
+ if (!fs.existsSync(targetPath)) {
1847
+ fs.renameSync(sourcePath, targetPath);
1848
+ return;
1849
+ }
1850
+ const source = readJsonFile(sourcePath);
1851
+ const target = readJsonFile(targetPath);
1852
+ if (source && target && !Array.isArray(source) && !Array.isArray(target)) {
1853
+ atomicWriteJson(targetPath, { ...source, ...target });
1854
+ fs.unlinkSync(sourcePath);
1855
+ return;
1856
+ }
1857
+ if (fs.readFileSync(sourcePath).equals(fs.readFileSync(targetPath))) {
1858
+ fs.unlinkSync(sourcePath);
1859
+ return;
1860
+ }
1861
+ fs.renameSync(sourcePath, this.uniqueLegacyFilePath(targetPath));
1862
+ }
1863
+ uniqueLegacyFilePath(targetPath) {
1864
+ let index = 1;
1865
+ let candidate = `${targetPath}.legacy-unknown`;
1866
+ while (fs.existsSync(candidate)) {
1867
+ candidate = `${targetPath}.legacy-unknown-${index++}`;
1868
+ }
1869
+ return candidate;
1870
+ }
1665
1871
  }
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'url';
3
3
  import { execFileSync, execFile, spawn, spawnSync } from 'child_process';
4
4
  import { promisify } from 'util';
5
5
  import fs from 'fs';
6
+ import { getProcessStartTime } from './process-introspect.js';
6
7
  const execFileAsync = promisify(execFile);
7
8
  export const isWindows = process.platform === 'win32';
8
9
  const ENCODE_PATH_MAX = 200;
@@ -133,10 +134,15 @@ export function getProcessInfo(pid) {
133
134
  }
134
135
  }
135
136
  else {
136
- const etimes = execFileSync('ps', ['-p', String(pid), '-o', 'etimes='], { encoding: 'utf-8' }).trim();
137
+ // procps `etimes` can overflow for freshly spawned processes in some
138
+ // container/time-namespace combinations. /proc start time is reliable
139
+ // on Linux and is already used for PID reuse protection elsewhere.
140
+ const startedAt = process.platform === 'linux' ? getProcessStartTime(pid) : null;
141
+ const uptime = startedAt !== null
142
+ ? formatUptime(Math.max(0, Math.floor((Date.now() - startedAt) / 1000)))
143
+ : formatUptime(parseInt(execFileSync('ps', ['-p', String(pid), '-o', 'etimes='], { encoding: 'utf-8' }).trim(), 10));
137
144
  const cpu = execFileSync('ps', ['-p', String(pid), '-o', '%cpu='], { encoding: 'utf-8' }).trim();
138
145
  const mem = execFileSync('ps', ['-p', String(pid), '-o', 'rss='], { encoding: 'utf-8' }).trim();
139
- const uptime = formatUptime(parseInt(etimes, 10));
140
146
  return { uptime, cpu, memory: mem };
141
147
  }
142
148
  }
@@ -8,8 +8,25 @@
8
8
  */
9
9
  import fs from 'fs';
10
10
  import { spawnSync } from 'child_process';
11
- import { isWindows } from './cross-platform.js';
11
+ const isWindows = process.platform === 'win32';
12
12
  const isMacOS = process.platform === 'darwin';
13
+ function readLinuxClockTicks() {
14
+ if (process.platform !== 'linux')
15
+ return 100;
16
+ try {
17
+ const result = spawnSync('getconf', ['CLK_TCK'], {
18
+ encoding: 'utf-8',
19
+ timeout: 1000,
20
+ stdio: ['ignore', 'pipe', 'ignore'],
21
+ });
22
+ const value = Number.parseInt(result.stdout?.trim() || '', 10);
23
+ return value > 0 ? value : 100;
24
+ }
25
+ catch {
26
+ return 100;
27
+ }
28
+ }
29
+ const linuxClockTicks = readLinuxClockTicks();
13
30
  /** 容差:2 秒(覆盖 macOS 秒级精度 + 时钟漂移) */
14
31
  export const START_TIME_TOLERANCE_MS = 2000;
15
32
  /**
@@ -72,8 +89,7 @@ function getStartTimeLinux(pid) {
72
89
  }
73
90
  if (isNaN(btimeSec))
74
91
  return null;
75
- const clkTck = 100;
76
- return (btimeSec + starttimeJiffies / clkTck) * 1000;
92
+ return (btimeSec + starttimeJiffies / linuxClockTicks) * 1000;
77
93
  }
78
94
  // ── macOS ──
79
95
  function getStartTimeMacOS(pid) {
@@ -134,15 +134,15 @@ ConfigManager 只合并上述物理层并返回物理来源。角色策略不进
134
134
  {
135
135
  "access": {
136
136
  "policyMode": "contacts",
137
- "allowedGroupChannels": [],
138
137
  "requestLimit": { "maxTimes": 600, "coolDown": 3600 }
139
138
  }
140
139
  }
141
140
  ```
142
141
 
143
- `policyMode` 为 `open | contacts | owners-only`;`allowedGroupChannels` 使用完整渠道实例键,空数组表示
144
- 关闭全部群聊渠道;`coolDown` 单位为秒。该对象由 Connect 的访问设置入口整体修改并与 Agent Owner、
145
- 已启用渠道实例共同校验,不通过通用 relation/defaults 覆盖链放宽。
142
+ `policyMode` 为 `open | contacts | owners`;`open` 对所有已配置渠道的私聊和群聊入站开放,`contacts`
143
+ 将 Agent 已加入并接收到的群聊视为授权,`owners` Owner 消息开放;群聊成员不按 Contact Book 联系人
144
+ 状态逐一过滤。`requestLimit` 只限制准入控制消息的自动回复、申请和绑定码提示,不是渠道准入开关;`coolDown`
145
+ 单位为秒。该对象由 Connect 的访问设置入口修改,不通过通用 relation/defaults 覆盖链放宽。
146
146
 
147
147
  ### AUN 群规则加载策略
148
148
 
@@ -56,6 +56,7 @@ ec contact list --blocked --self <agent-aid>
56
56
  ## 拉黑后的消息行为
57
57
 
58
58
  - AUN 和已绑定外部私聊身份的入站在 MessageBridge 业务入口被拦截,不进入 menu/auth/command/session/queue/LLM。
59
- - 群聊不按成员的联系人状态过滤,只由 Agent 的 `access.allowedGroupChannels` `policyMode` 决定。
59
+ - 群聊不按成员的联系人状态过滤;`open` 对所有已配置渠道的群聊入站开放,`contacts` Agent 已加入并
60
+ 接收到的群聊视为授权,`owners` 仅 Owner 消息开放。
60
61
  - 对 `ai`、`bot`、`agent`、`service` 类型的私聊对端,可限频返回 `[rejection] Not accepting messages from you.`。
61
62
  - 入站正文中的 `[rejection]` 没有协议控制语义。发送者未被本 Agent 拉黑时,该文本正常进入响应式会话。
@@ -95,7 +95,6 @@
95
95
  "additionalProperties": false,
96
96
  "required": [
97
97
  "policyMode",
98
- "allowedGroupChannels",
99
98
  "requestLimit"
100
99
  ],
101
100
  "properties": {
@@ -104,18 +103,9 @@
104
103
  "enum": [
105
104
  "open",
106
105
  "contacts",
107
- "owners-only"
106
+ "owners"
108
107
  ],
109
- "description": "私聊准入策略:开放、仅联系人和 Owner、或仅 Owner。"
110
- },
111
- "allowedGroupChannels": {
112
- "type": "array",
113
- "items": {
114
- "type": "string",
115
- "minLength": 1
116
- },
117
- "uniqueItems": true,
118
- "description": "允许群消息进入的完整渠道实例键;空数组表示关闭所有群聊渠道。"
108
+ "description": "私聊和群聊准入策略:开放、仅联系人或仅 Owner。"
119
109
  },
120
110
  "requestLimit": {
121
111
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolcore",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "AI Agent gateway connecting multiple backends (Claude, Codex, Gemini) to messaging channels (Feishu, WeChat, AUN, QQ) with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",