feat(h5): web 联网能力、实时查询路由与 session Finish 对齐
- 新增 web 能力并挂载 platform/web(web_search/fetch_url) - 实时查询强制 web skill,router fallback 与 await session Finish - Session Broker 覆盖率/指标、stream replay 与相关单测/E2E 脚本 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
/**
|
||||
* SESSION BROKER COVERAGE (H5 Session architecture Patch 2 / §5.7)
|
||||
*
|
||||
* Business code must not call userAuth session ownership helpers directly.
|
||||
* Use session-broker.mjs facade via createSessionAccess / resolveSessionAccess.
|
||||
*
|
||||
* See docs/h5-session-architecture-20260706.md §5.7
|
||||
*/
|
||||
|
||||
/** @typedef {{ file: string, line: number, rule: string, text: string }} SessionBrokerCoverageViolation */
|
||||
|
||||
export const SESSION_BROKER_FACADE_FILE = 'session-broker.mjs';
|
||||
export const SESSION_BROKER_DEFINITION_FILE = 'user-auth.mjs';
|
||||
|
||||
/** Files allowed to call userAuth.registerAgentSession / ownsSession directly. */
|
||||
export const SESSION_BROKER_DIRECT_ALLOWLIST = new Set([
|
||||
SESSION_BROKER_FACADE_FILE,
|
||||
SESSION_BROKER_DEFINITION_FILE,
|
||||
]);
|
||||
|
||||
/** Patch 2 modules that must route session ownership through sessionAccess. */
|
||||
export const SESSION_BROKER_PATCH2_FILES = [
|
||||
'tkmind-proxy.mjs',
|
||||
'agent-run-routes.mjs',
|
||||
'agent-run-gateway.mjs',
|
||||
'direct-chat-service.mjs',
|
||||
'server.mjs',
|
||||
'wechat-mp.mjs',
|
||||
'mindspace-page-edit-session.mjs',
|
||||
'mindspace-agent-runner.mjs',
|
||||
'mindspace-conversation-package-routes.mjs',
|
||||
];
|
||||
|
||||
export const SESSION_BROKER_FORBIDDEN_CALLS = [
|
||||
{ id: 'register-agent-session', pattern: /\buserAuth\.registerAgentSession\s*\(/ },
|
||||
{ id: 'owns-session', pattern: /\buserAuth\.ownsSession\s*\(/ },
|
||||
{ id: 'get-session-target', pattern: /\buserAuth\.getSessionTarget\s*\(/ },
|
||||
{ id: 'unregister-agent-session', pattern: /\buserAuth\.unregisterAgentSession\s*\(/ },
|
||||
];
|
||||
|
||||
export const SESSION_BROKER_PATCH2_MARKERS = [
|
||||
/\bresolveSessionAccess\b/,
|
||||
/\bcreateSessionAccess\b/,
|
||||
/\bsessionAccess\b/,
|
||||
];
|
||||
|
||||
export const SESSION_BROKER_SCAN_IGNORE = [
|
||||
/^scripts\//,
|
||||
/^mindspace-service\//,
|
||||
/^\.runtime\//,
|
||||
/^node_modules\//,
|
||||
/\.test\.mjs$/,
|
||||
/\.test\.ts$/,
|
||||
];
|
||||
|
||||
export const SESSION_BROKER_LINE_ALLOW = [
|
||||
/SESSION BROKER COVERAGE/,
|
||||
/§5\.7/,
|
||||
/session-broker\.mjs/,
|
||||
/createSessionAccess/,
|
||||
/resolveSessionAccess/,
|
||||
/sessionAccess\./,
|
||||
/getSessionAccess/,
|
||||
];
|
||||
|
||||
function relativePosix(rootDir, absolutePath) {
|
||||
return path.relative(rootDir, absolutePath).split(path.sep).join('/');
|
||||
}
|
||||
|
||||
function isIgnoredScanPath(relativePath) {
|
||||
return SESSION_BROKER_SCAN_IGNORE.some((pattern) => pattern.test(relativePath));
|
||||
}
|
||||
|
||||
function isTestFile(relativePath) {
|
||||
return /\.test\.mjs$/.test(relativePath);
|
||||
}
|
||||
|
||||
function isLineAllowed(line) {
|
||||
return SESSION_BROKER_LINE_ALLOW.some((pattern) => pattern.test(line));
|
||||
}
|
||||
|
||||
function scanDirectUserAuthCalls(relativePath, content) {
|
||||
/** @type {SessionBrokerCoverageViolation[]} */
|
||||
const violations = [];
|
||||
if (SESSION_BROKER_DIRECT_ALLOWLIST.has(relativePath) || isTestFile(relativePath)) {
|
||||
return violations;
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
if (!line.trim() || line.trim().startsWith('//') || line.trim().startsWith('*')) continue;
|
||||
if (isLineAllowed(line)) continue;
|
||||
for (const rule of SESSION_BROKER_FORBIDDEN_CALLS) {
|
||||
if (!rule.pattern.test(line)) continue;
|
||||
violations.push({
|
||||
file: relativePath,
|
||||
line: index + 1,
|
||||
rule: rule.id,
|
||||
text: line.trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
function checkPatch2ModuleWiring(rootDir) {
|
||||
/** @type {SessionBrokerCoverageViolation[]} */
|
||||
const violations = [];
|
||||
for (const relativePath of SESSION_BROKER_PATCH2_FILES) {
|
||||
const absolutePath = path.join(rootDir, relativePath);
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
violations.push({
|
||||
file: relativePath,
|
||||
line: 0,
|
||||
rule: 'missing-patch2-file',
|
||||
text: 'Patch 2 module missing from repository',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const content = fs.readFileSync(absolutePath, 'utf8');
|
||||
if (!SESSION_BROKER_PATCH2_MARKERS.some((pattern) => pattern.test(content))) {
|
||||
violations.push({
|
||||
file: relativePath,
|
||||
line: 0,
|
||||
rule: 'missing-session-access-wiring',
|
||||
text: 'Expected resolveSessionAccess/createSessionAccess/sessionAccess wiring',
|
||||
});
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
export function checkSessionBrokerCoverage(rootDir, { scanRepo = true } = {}) {
|
||||
/** @type {SessionBrokerCoverageViolation[]} */
|
||||
const violations = [...checkPatch2ModuleWiring(rootDir)];
|
||||
|
||||
if (!scanRepo) return violations;
|
||||
|
||||
const queue = [rootDir];
|
||||
while (queue.length > 0) {
|
||||
const current = queue.pop();
|
||||
const entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
||||
const absolutePath = path.join(current, entry.name);
|
||||
const relativePath = relativePosix(rootDir, absolutePath);
|
||||
if (entry.isDirectory()) {
|
||||
queue.push(absolutePath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.name.endsWith('.mjs')) continue;
|
||||
if (isIgnoredScanPath(relativePath)) continue;
|
||||
const content = fs.readFileSync(absolutePath, 'utf8');
|
||||
violations.push(...scanDirectUserAuthCalls(relativePath, content));
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
export function formatSessionBrokerCoverageViolations(violations) {
|
||||
return violations
|
||||
.map((item) => `${item.file}:${item.line} [${item.rule}] ${item.text}`)
|
||||
.join('\n');
|
||||
}
|
||||
Reference in New Issue
Block a user