@bolloon/bolloon-agent 0.5.0 → 0.5.2
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/README.md +39 -0
- package/dist/agents/agent-lang.js +101 -0
- package/dist/agents/agent-manifest-protocol.js +4 -0
- package/dist/agents/execution-supervisor.js +87 -5
- package/dist/agents/gateway-group.js +44 -3
- package/dist/agents/model-policy.js +651 -0
- package/dist/agents/pi-sdk-tools.js +84 -33
- package/dist/agents/pi-sdk.js +184 -31
- package/dist/agents/run-store.js +53 -0
- package/dist/agents/task/task-runner.js +8 -1
- package/dist/agents/workflow-pivot-loop.js +22 -13
- package/dist/cli/clipboard.js +65 -0
- package/dist/cli/ink-app.js +145 -17
- package/dist/cli/log-gate.js +386 -0
- package/dist/cli/mention-data.js +2 -0
- package/dist/cli/model-selector.js +538 -0
- package/dist/cli/reply-hygiene.js +54 -0
- package/dist/cli/setup-wizard.js +686 -69
- package/dist/cli/startup-notice.js +160 -0
- package/dist/cli/stores.js +22 -1
- package/dist/cli/theme.js +49 -0
- package/dist/cli/tui-select.js +970 -0
- package/dist/cli-entry.js +125 -18
- package/dist/efficode/compress.js +78 -0
- package/dist/efficode/crc32.js +38 -0
- package/dist/efficode/did.js +66 -0
- package/dist/efficode/index.js +24 -0
- package/dist/efficode/lz77.js +170 -0
- package/dist/efficode/negotiate.js +187 -0
- package/dist/efficode/ops.js +87 -0
- package/dist/efficode/packet.js +271 -0
- package/dist/efficode/types.js +45 -0
- package/dist/electron-build/agents/agent-manifest-protocol.js +4 -0
- package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
- package/dist/electron-build/agents/execution-supervisor.js +86 -4
- package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/model-policy.js +706 -0
- package/dist/electron-build/agents/model-policy.js.map +1 -0
- package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +185 -31
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +55 -1
- package/dist/electron-build/agents/run-store.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +8 -1
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/workflow-pivot-loop.js +22 -13
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/llm/config-store.js +146 -6
- package/dist/electron-build/llm/config-store.js.map +1 -1
- package/dist/electron-build/llm/connection-probe.js +764 -0
- package/dist/electron-build/llm/connection-probe.js.map +1 -0
- package/dist/electron-build/llm/custom-provider-store.js +272 -0
- package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
- package/dist/electron-build/llm/model-catalog.js +778 -0
- package/dist/electron-build/llm/model-catalog.js.map +1 -0
- package/dist/electron-build/llm/model-discovery.js +1202 -0
- package/dist/electron-build/llm/model-discovery.js.map +1 -0
- package/dist/electron-build/llm/model-selection.js +1315 -0
- package/dist/electron-build/llm/model-selection.js.map +1 -0
- package/dist/electron-build/llm/pi-ai.js +751 -55
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/llm/provider-catalog.js +969 -0
- package/dist/electron-build/llm/provider-catalog.js.map +1 -0
- package/dist/electron-build/llm/provider-registry.js +950 -0
- package/dist/electron-build/llm/provider-registry.js.map +1 -0
- package/dist/electron-build/llm/tool-name.js +266 -0
- package/dist/electron-build/llm/tool-name.js.map +1 -0
- package/dist/electron-build/utils/dual-source-facts.js +236 -0
- package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
- package/dist/electron-build/utils/dual-source.js +32 -156
- package/dist/electron-build/utils/dual-source.js.map +1 -1
- package/dist/electron-build/utils/update-manager.js +22 -72
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +2 -19
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-identity.js +193 -0
- package/dist/electron-build/utils/version-identity.js.map +1 -0
- package/dist/electron-build/utils/version-info.js +47 -101
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +282 -96
- package/dist/ios/bolloon-web.json +10 -0
- package/dist/ios/mobile-contacts.js +44 -11
- package/dist/ios/mobile-core.js +1723 -84
- package/dist/ios/mobile-tasks.js +339 -0
- package/dist/ios/mobile-update.js +1442 -0
- package/dist/ios/mobile.js +308 -35
- package/dist/ios/routes-llm-config.js +344 -52
- package/dist/ios/routes-mobile-tasks.js +353 -0
- package/dist/ios/server.js +163 -4
- package/dist/ios/sw.js +1 -1
- package/dist/llm/config-store.js +146 -6
- package/dist/llm/connection-probe.js +744 -0
- package/dist/llm/custom-provider-store.js +224 -0
- package/dist/llm/data/provider-catalog-baked.js +19 -0
- package/dist/llm/model-catalog.js +717 -0
- package/dist/llm/model-discovery.js +1137 -0
- package/dist/llm/model-selection.js +1241 -0
- package/dist/llm/pi-ai.js +732 -54
- package/dist/llm/provider-catalog.js +898 -0
- package/dist/llm/provider-registry.js +917 -0
- package/dist/llm/tool-name.js +220 -0
- package/dist/setup/onboard.js +34 -8
- package/dist/utils/dual-source-facts.js +223 -0
- package/dist/utils/dual-source.js +12 -141
- package/dist/utils/update-manager.js +16 -59
- package/dist/utils/update-state.js +6 -18
- package/dist/utils/version-identity.js +176 -0
- package/dist/utils/version-info.js +9 -78
- package/dist/web/bolloon-web.json +10 -0
- package/dist/web/mobile-core.js +1721 -84
- package/dist/web/mobile-update.js +1442 -0
- package/dist/web/mobile.js +372 -35
- package/dist/web/routes-llm-config.js +344 -52
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/scripts/build-web.ts +36 -0
package/dist/cli-entry.js
CHANGED
|
@@ -22,6 +22,9 @@ import { discoverEngines, delegateToEngine } from './external-engines/index.js';
|
|
|
22
22
|
import { x402CheckBalance, x402Fetch } from './agents/x402/x402Pay.js';
|
|
23
23
|
import { runVersionCommand, runUpdateCommand, runDoctorCommand, runRuntimeCommand, UPDATE_HELP } from './cli/update-commands.js';
|
|
24
24
|
import { collectVersionInfo } from './utils/version-info.js';
|
|
25
|
+
// 2026-09-27: 启动前言的默认静默 + 显式查询命令 (与 `src/index.ts` 同一套判断)
|
|
26
|
+
import { startupPreambleVisible } from './cli/startup-notice.js';
|
|
27
|
+
import { logStartupLine } from './cli/log-gate.js';
|
|
25
28
|
// 2026-09-21 (P3): 统一 JSON 信封 + 命令组 (network/agent/task/wallet/payment/trade)
|
|
26
29
|
import { runServiceGroup, GROUPS_HELP } from './cli/commands/index.js';
|
|
27
30
|
// 2026-09-21 (P4): MCP 适配层 (`bolloon mcp serve` = stdio, 只调 P3 服务层)
|
|
@@ -71,6 +74,9 @@ ${BOLD}命令:${RESET}
|
|
|
71
74
|
bolloon model [name] [model] 列出 / 切换模型供应商 (如: bolloon model deepseek deepseek-v4-flash)
|
|
72
75
|
bolloon model key <name> 配置某供应商的 API key (隐藏输入, 不回显)
|
|
73
76
|
bolloon model test [name] 测试供应商连通性
|
|
77
|
+
bolloon model list [name] 看模型发现目录 (P5)
|
|
78
|
+
bolloon model refresh [name] 重取上游模型目录 (--clear 清缓存)
|
|
79
|
+
bolloon model admit <name> <model> 手输一个目录里没有的模型名
|
|
74
80
|
bolloon read <file> 读取文档
|
|
75
81
|
bolloon summarize <file> 总结文档
|
|
76
82
|
bolloon improve <file> <req> 改进文档
|
|
@@ -568,36 +574,121 @@ async function handleP2pCommand(p2pArgs) {
|
|
|
568
574
|
// 那是常驻模块, 句柄不会自己关 → 不显式退出会"输出完了还挂着"。
|
|
569
575
|
process.exit(info.ok ? 0 : 1);
|
|
570
576
|
}
|
|
577
|
+
/** 真终端判定: stdin 与 stdout 都是 TTY 才算交互 (管道 / 重定向 = 脚本可读输出) */
|
|
578
|
+
function isRealTty() {
|
|
579
|
+
return !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* `bolloon model [...]` — 模型供应商/模型切换的唯一命令行入口。
|
|
583
|
+
*
|
|
584
|
+
* leo 2026-09-27 口径: **`bolloon model` 就是"切换"的启动命令** —— 真终端里敲完直接就是选择界面
|
|
585
|
+
* (与 `bolloon model pick` 同一实现, 只是用户不必知道 pick 这个词), 第一屏就是带序号/状态/当前项
|
|
586
|
+
* 标记的供应商列表, 选完往下走; **不许**先刷一坨供应商清单+用法再问。
|
|
587
|
+
* 管道/非 TTY 才退回清单+用法 (脚本可读, 不会卡在等待输入)。
|
|
588
|
+
* `pick` / `list` 两个子命令都保留 (pick 是显式写法, list 只读列表)。
|
|
589
|
+
*/
|
|
571
590
|
async function handleModelCommand(modelArgs) {
|
|
572
|
-
const { llmConfigStore
|
|
591
|
+
const { llmConfigStore } = await import('./llm/config-store.js');
|
|
592
|
+
const { effectiveModelConfig, formatEffectiveModel } = await import('./llm/model-selection.js');
|
|
573
593
|
await llmConfigStore.initialize();
|
|
574
|
-
|
|
594
|
+
const tty = isRealTty();
|
|
595
|
+
// ── 真终端 + 裸敲: 直接进选择器 (第一屏 = 供应商列表) ──────────
|
|
596
|
+
if (modelArgs.length === 0 && tty) {
|
|
597
|
+
const { runModelCommand, modelTtyIO, askHiddenLineEof, askLineEof } = await import('./cli/setup-wizard.js');
|
|
598
|
+
const out = await runModelCommand('pick', await useTui() ? modelTtyIO() : {
|
|
599
|
+
live: true, // 每一步当场打到终端 (不是等整轮结束才一次性回显)
|
|
600
|
+
print: (l) => process.stdout.write(l + '\n'),
|
|
601
|
+
askHidden: async (q) => { const r = await askHiddenLineEof(q); return r.eof ? null : r.value; },
|
|
602
|
+
ask: async (q, opts) => { const r = await askLineEof(q, opts); return r.eof ? null : r.value; },
|
|
603
|
+
});
|
|
604
|
+
for (const line of String(out).split('\n'))
|
|
605
|
+
if (line !== '')
|
|
606
|
+
console.log(line);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
// ── 无参 + 非终端 (管道/脚本): 列出**全部**供应商 (分组带家数) + 当前**真实生效**的那一份 + 用法 ──
|
|
575
610
|
if (modelArgs.length === 0) {
|
|
576
|
-
const
|
|
577
|
-
|
|
611
|
+
const eff = await effectiveModelConfig({}).catch(() => null);
|
|
612
|
+
const mc = await import('./llm/model-catalog.js');
|
|
613
|
+
const summaries = await mc.buildProviderSummaries({});
|
|
614
|
+
const pc = await import('./llm/provider-catalog.js');
|
|
615
|
+
await pc.initializeProviderCatalog();
|
|
616
|
+
console.log(`\n${BOLD}模型供应商${RESET} (当前生效: ${eff ? `${eff.provider}/${eff.model}` : '读不出来'})\n`);
|
|
617
|
+
if (eff)
|
|
618
|
+
console.log(` ${formatEffectiveModel(eff)}`);
|
|
578
619
|
console.log('─'.repeat(58));
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
620
|
+
// 非终端里没有"固定高度视窗 + 折叠"那套交互 (那是全屏 TUI 的事), 所以这里**整份列全**:
|
|
621
|
+
// 内置 13 + 自定义 + 目录全部 逐行出, 分组标题带家数。只给"分组计数"是这条线要修的病 ——
|
|
622
|
+
// 脚本要能 grep 到任何一家 (含"需专用鉴权 (未支持)""无基址"的那些)。
|
|
623
|
+
const ordered = mc.orderProvidersForMenu(summaries);
|
|
624
|
+
let lastGroup = '';
|
|
625
|
+
for (const s of ordered) {
|
|
626
|
+
const tier = mc.providerTierOf(s);
|
|
627
|
+
const g = mc.PROVIDER_GROUPS[tier];
|
|
628
|
+
if (g !== lastGroup) {
|
|
629
|
+
lastGroup = g;
|
|
630
|
+
const n = ordered.filter((x) => mc.providerTierOf(x) === tier).length;
|
|
631
|
+
console.log(` ── ${g} (${n} 家)${mc.providerTierCollapsedByDefault(tier) ? ' · 交互界面里默认收起, 空格/→ 展开' : ''}`);
|
|
632
|
+
}
|
|
633
|
+
console.log(` ${mc.formatProviderMenuRow(s)}`);
|
|
585
634
|
}
|
|
635
|
+
console.log(` ${mc.providerGroupSummary(summaries)}`);
|
|
636
|
+
console.log(` ${pc.catalogStatusLine()}`);
|
|
637
|
+
console.log(` ${pc.catalogGroupLine()}`);
|
|
586
638
|
console.log(`\n${BOLD}用法:${RESET}`);
|
|
639
|
+
console.log(` bolloon model # 终端里直接进选择界面 (固定高度视窗 + 分组折叠; 上面这份全量列表是管道/脚本的可读输出)`);
|
|
640
|
+
console.log(` bolloon model pick # 同上, 显式写法 (供应商→凭证→模型→参数→作用域→测试→确认)`);
|
|
587
641
|
console.log(` bolloon model <name> # 切换到该供应商`);
|
|
588
642
|
console.log(` bolloon model <name> <model> # 切换并指定模型`);
|
|
643
|
+
console.log(` bolloon model list [name] # 看模型发现目录 (只读)`);
|
|
644
|
+
console.log(` bolloon model refresh [name] # 重取上游目录 (--clear 清缓存)`);
|
|
645
|
+
console.log(` bolloon model admit <name> <m> # 手输一个目录里没有的模型名`);
|
|
646
|
+
console.log(` bolloon model catalog # 供应商目录: 是几号的/新鲜度/族分布 (只读)`);
|
|
647
|
+
console.log(` bolloon model catalog list [词] # 逐家看 (**默认全量**, 含"需专用鉴权/无基址"并标原因; --usable 只看能用的)`);
|
|
648
|
+
console.log(` bolloon model catalog refresh # 运行期真拉最新目录 (落盘 0600, 记来源与时间)`);
|
|
589
649
|
console.log(` 示例: bolloon model deepseek deepseek-v4-flash`);
|
|
590
650
|
return;
|
|
591
651
|
}
|
|
592
|
-
// 有参: 统一交给 setup-wizard 的 runModelCommand
|
|
593
|
-
// (切换 / <provider> <model> / key <provider> / test / status 一套语义,
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
652
|
+
// ── 有参: 统一交给 setup-wizard 的 runModelCommand ───────────
|
|
653
|
+
// (pick 分步选择 / 切换 / <provider> <model> / key <provider> / test / status 一套语义,
|
|
654
|
+
// 与 CLI 会话内 /model 完全一致 —— 写配置与重建运行时的逻辑只有 selectModel 一处)
|
|
655
|
+
const { runModelCommand, modelTtyIO, askHiddenLineEof, askLineEof } = await import('./cli/setup-wizard.js');
|
|
656
|
+
// `--verbose` 交给界面层决定"打不打内部细节", 不进参数解析 (解析器不认识这个词会被当值)
|
|
657
|
+
const wantVerbose = modelArgs.includes('--verbose');
|
|
658
|
+
const args = modelArgs.filter((a) => a !== '--verbose');
|
|
659
|
+
const out = await runModelCommand(args.join(' '), (await useTui()) && tty ? {
|
|
660
|
+
...modelTtyIO(),
|
|
661
|
+
...(wantVerbose ? { verbose: true } : {}),
|
|
662
|
+
} : {
|
|
663
|
+
// 非 TTY (管道/脚本) / 显式关掉 TUI: 逐行问答 —— 输出可读、不卡死、行为与以前一致
|
|
664
|
+
live: tty,
|
|
665
|
+
print: (l) => process.stdout.write(l + '\n'),
|
|
666
|
+
askHidden: async (q) => { const r = await askHiddenLineEof(q); return r.eof ? null : r.value; },
|
|
667
|
+
ask: async (q, opts) => { const r = await askLineEof(q, opts); return r.eof ? null : r.value; },
|
|
668
|
+
...(wantVerbose ? { verbose: true } : {}),
|
|
598
669
|
});
|
|
599
670
|
for (const line of String(out).split('\n'))
|
|
600
|
-
|
|
671
|
+
if (line !== '')
|
|
672
|
+
console.log(line);
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* 这个环境要不要用全屏 TUI 选择器。
|
|
676
|
+
* 三判据: ① 用户没显式关 (`BOLLOON_NO_TUI=1`); ② 真 TTY; ③ `tuiCapable()` (终端够高/能拿 raw mode)。
|
|
677
|
+
* 任何一条不满足 → 退回逐行问答 (**不静默变样子**: 管道输出仍是可读清单+用法)。
|
|
678
|
+
*/
|
|
679
|
+
function useTui() {
|
|
680
|
+
return (async () => {
|
|
681
|
+
if (process.env.BOLLOON_NO_TUI)
|
|
682
|
+
return false;
|
|
683
|
+
try {
|
|
684
|
+
// 同步判据在 tui-select 里 (它不许在非 TTY 里抢输入)
|
|
685
|
+
const { tuiCapable } = await import('./cli/tui-select.js');
|
|
686
|
+
return !!tuiCapable();
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
})();
|
|
601
692
|
}
|
|
602
693
|
/** `bolloon setup` — 首次运行初始化向导 (用户身份 + 模型供应商 + API key + 连通性测试) */
|
|
603
694
|
async function handleSetupCommand(setupArgs) {
|
|
@@ -615,6 +706,7 @@ async function handleSetupCommand(setupArgs) {
|
|
|
615
706
|
console.log(`${BOLD}bolloon setup${RESET} — 初始化 Bolloon (用户身份 + 模型供应商 + API key)`);
|
|
616
707
|
console.log('');
|
|
617
708
|
console.log(' 无参数 交互式向导 (推荐)');
|
|
709
|
+
console.log(' status 只读: 看初始化状态/门禁/就绪度全量 (启动面板那一行"详情"指的就是它)');
|
|
618
710
|
console.log(' --provider <名> 指定供应商 (deepseek / minimax / openai / anthropic / ...)');
|
|
619
711
|
console.log(' --api-key <key> 直接给 key (脚本用; 交互模式会隐藏输入)');
|
|
620
712
|
console.log(' --model <名> 指定模型');
|
|
@@ -623,6 +715,15 @@ async function handleSetupCommand(setupArgs) {
|
|
|
623
715
|
console.log(' repair-runtime 检查并补装缺失运行时 (Node/npm/Git/Python)');
|
|
624
716
|
return;
|
|
625
717
|
}
|
|
718
|
+
// 2026-09-27: `bolloon setup status` —— **启动前言搬去的地方**。
|
|
719
|
+
// 启动时那一整段 (初始化状态/就绪度/已完成/配置来源/已存输入/缺什么/下一步) 默认不上屏,
|
|
720
|
+
// 要看全量就显式敲这条 (只读, 不写任何文件; 退出码 0 = ready, 1 = 未就绪 —— 脚本可用)。
|
|
721
|
+
if (setupArgs[0] === 'status' || setupArgs.includes('--status')) {
|
|
722
|
+
const { evaluateSetup, describeSetup } = await import('./setup/setup-store.js');
|
|
723
|
+
const ev = await evaluateSetup();
|
|
724
|
+
process.stdout.write(describeSetup(ev) + '\n');
|
|
725
|
+
process.exit(ev.gate === 'ready' ? 0 : 1);
|
|
726
|
+
}
|
|
626
727
|
const apiKey = val('--api-key');
|
|
627
728
|
const needsInteractive = !apiKey;
|
|
628
729
|
const r = await runSetupWizard({
|
|
@@ -801,7 +902,13 @@ async function startWebServer(additionalArgs) {
|
|
|
801
902
|
}
|
|
802
903
|
// 启动 CLI
|
|
803
904
|
async function startCLI(additionalArgs) {
|
|
804
|
-
|
|
905
|
+
// 2026-09-27 (leo 口径: 打完 `bolloon --cli` 直接看到面板): 这句"启动命令行界面..."属于**启动前言**,
|
|
906
|
+
// 默认不上屏 (照落启动日志); `--verbose` / `BOLLOON_VERBOSE=1` / `BOLLOON_STARTUP_PREAMBLE=1` 逐字回来。
|
|
907
|
+
// (gui/web 那两个面的 banner 不动 —— 它们没有"面板", 输出就是交付物。)
|
|
908
|
+
if (startupPreambleVisible())
|
|
909
|
+
log('启动命令行界面...', CYAN);
|
|
910
|
+
else
|
|
911
|
+
logStartupLine('启动命令行界面...');
|
|
805
912
|
try {
|
|
806
913
|
const mainPath = getMainScript();
|
|
807
914
|
const cliArgs = [...additionalArgs];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* efficode/compress - 可插拔压缩层
|
|
3
|
+
*
|
|
4
|
+
* 三个算法 (数据块第一字节 = 算法 id):
|
|
5
|
+
* 0 none 原样 (小消息用 —— 头上开箱代价为 0)
|
|
6
|
+
* 1 lz77 本仓实现, 纯 JS 可跑任何运行面 (含手机 WebView)
|
|
7
|
+
* 2 deflate node:zlib deflateRaw (真压缩, 真数字)
|
|
8
|
+
*
|
|
9
|
+
* 默认策略 `pickAlgo`: 先按内容选, 选完**真压一遍**, 压不小就退回 `none`
|
|
10
|
+
* (小消息硬压一定更贵 —— 这是实测结论, 见 wiki §"更高效吗")。
|
|
11
|
+
*/
|
|
12
|
+
import { deflateRawSync, inflateRawSync } from 'node:zlib';
|
|
13
|
+
import { lz77Compress, lz77Decompress } from './lz77.js';
|
|
14
|
+
import { COMPRESSION_IDS, COMPRESSION_BY_ID, EfficodeError } from './types.js';
|
|
15
|
+
export const COMPRESSION_ALGOS = ['none', 'lz77', 'deflate'];
|
|
16
|
+
/** 压缩。返回 [1B 算法 id][载荷] 的完整数据块体 */
|
|
17
|
+
export function compressBlock(raw, algo) {
|
|
18
|
+
const body = compressPayload(raw, algo);
|
|
19
|
+
const out = new Uint8Array(body.length + 1);
|
|
20
|
+
out[0] = COMPRESSION_IDS[algo];
|
|
21
|
+
out.set(body, 1);
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function compressPayload(raw, algo) {
|
|
25
|
+
switch (algo) {
|
|
26
|
+
case 'none':
|
|
27
|
+
return raw;
|
|
28
|
+
case 'lz77':
|
|
29
|
+
return lz77Compress(raw);
|
|
30
|
+
case 'deflate':
|
|
31
|
+
return new Uint8Array(deflateRawSync(raw));
|
|
32
|
+
default:
|
|
33
|
+
throw new EfficodeError('EFFICODE_UNKNOWN_COMPRESSION', `未知压缩算法 ${String(algo)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** 解压 [1B 算法 id][载荷]。未知算法 id → 报错, 绝不"跳过解压直接给原文" */
|
|
37
|
+
export function decompressBlock(block) {
|
|
38
|
+
if (block.length < 1)
|
|
39
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', '数据块空 (缺算法 id)');
|
|
40
|
+
const algo = COMPRESSION_BY_ID[block[0]];
|
|
41
|
+
if (!algo) {
|
|
42
|
+
throw new EfficodeError('EFFICODE_UNKNOWN_COMPRESSION', `数据块声明未知压缩算法 id=${block[0]}`, 0);
|
|
43
|
+
}
|
|
44
|
+
const payload = block.subarray(1);
|
|
45
|
+
switch (algo) {
|
|
46
|
+
case 'none':
|
|
47
|
+
return { algo, data: payload.slice() };
|
|
48
|
+
case 'lz77':
|
|
49
|
+
return { algo, data: lz77Decompress(payload) };
|
|
50
|
+
case 'deflate': {
|
|
51
|
+
try {
|
|
52
|
+
return { algo, data: new Uint8Array(inflateRawSync(payload)) };
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', `deflate 解压失败: ${String(e?.message ?? e).slice(0, 120)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
default:
|
|
59
|
+
throw new EfficodeError('EFFICODE_UNKNOWN_COMPRESSION', `未知压缩算法 ${String(algo)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 默认算法选择: 结构化文本 (重复多) 用 deflate; 其余先用 lz77 试;
|
|
64
|
+
* 压缩后**不小于**原长就退回 none (省下的不该是负数)。
|
|
65
|
+
*/
|
|
66
|
+
export function pickAlgo(raw, preferred = 'deflate') {
|
|
67
|
+
if (raw.length < 32)
|
|
68
|
+
return 'none'; // 小消息: 头上开箱代价 > 收益
|
|
69
|
+
const got = compressPayload(raw, preferred);
|
|
70
|
+
return got.length < raw.length ? preferred : 'none';
|
|
71
|
+
}
|
|
72
|
+
/** 真测: 三个算法分别压出来的字节数 (给门和 wiki 用真实数字, 不许估) */
|
|
73
|
+
export function measureAll(raw) {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const a of COMPRESSION_ALGOS)
|
|
76
|
+
out[a] = compressPayload(raw, a).length + 1; // +1 = 算法 id 字节
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* efficode/crc32 - 包尾 CRC32 (IEEE 802.3, 多项式 0xEDB88320)
|
|
3
|
+
*
|
|
4
|
+
* 为什么自己写而不是用 zlib.crc32: 包层要能在**任何**运行面 (含手机 WebView 的
|
|
5
|
+
* 纯 JS 上下文) 真算同一个校验值, 所以这里是不依赖 node:zlib 的纯实现。
|
|
6
|
+
* 自证向量写在 src/test/efficode.test.ts: crc32("123456789") === 0xCBF43926 (规范值)。
|
|
7
|
+
*/
|
|
8
|
+
const TABLE = (() => {
|
|
9
|
+
const t = new Uint32Array(256);
|
|
10
|
+
for (let n = 0; n < 256; n++) {
|
|
11
|
+
let c = n;
|
|
12
|
+
for (let k = 0; k < 8; k++)
|
|
13
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
14
|
+
t[n] = c >>> 0;
|
|
15
|
+
}
|
|
16
|
+
return t;
|
|
17
|
+
})();
|
|
18
|
+
/** 算 CRC32, 返回无符号 32 位整数 */
|
|
19
|
+
export function crc32(buf) {
|
|
20
|
+
let c = 0xffffffff;
|
|
21
|
+
for (let i = 0; i < buf.length; i++)
|
|
22
|
+
c = TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
23
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
24
|
+
}
|
|
25
|
+
/** 打包成 4 字节大端 */
|
|
26
|
+
export function crc32Bytes(buf) {
|
|
27
|
+
const v = crc32(buf);
|
|
28
|
+
const out = new Uint8Array(4);
|
|
29
|
+
out[0] = (v >>> 24) & 0xff;
|
|
30
|
+
out[1] = (v >>> 16) & 0xff;
|
|
31
|
+
out[2] = (v >>> 8) & 0xff;
|
|
32
|
+
out[3] = v & 0xff;
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** 从 4 字节大端读回 */
|
|
36
|
+
export function readCrc32Bytes(b, offset = 0) {
|
|
37
|
+
return (((b[offset] << 24) | (b[offset + 1] << 16) | (b[offset + 2] << 8) | b[offset + 3]) >>> 0);
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* efficode/did - DID 验证段 (固定 32B)
|
|
3
|
+
*
|
|
4
|
+
* 布局 (32 字节):
|
|
5
|
+
* [0] algo id (= 1, sha256-截断)
|
|
6
|
+
* [1..3] 原始身份串字节数 (24 位大端, 上限 16MB)
|
|
7
|
+
* [4..31] sha256(身份串) 的**前 28 字节**
|
|
8
|
+
*
|
|
9
|
+
* 诚实边界 (必须写进 wiki, 不许含糊):
|
|
10
|
+
* - 32B **装不下**一个 DID / 公钥, 所以本段是**摘要**, 只能做"是不是同一个身份"的比对,
|
|
11
|
+
* **不能**从段里还原身份, 也**不能**独立证明持有私钥;
|
|
12
|
+
* - 真正的"链上签名验证 + ECC 临时会话密钥"属**规范中/未实现** —— 本模块没有签名函数,
|
|
13
|
+
* 也没有密钥交换函数 (不写假实现)。签名校验的接线点在 packet 层的 `verifyDid` 回调,
|
|
14
|
+
* 现成可用的只有摘要比对这一条。
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import { DID_SEGMENT_BYTES, EfficodeError } from './types.js';
|
|
18
|
+
export const DID_ALGO_SHA256_TRUNC28 = 1;
|
|
19
|
+
/** 编: 身份串 → 32B 段 */
|
|
20
|
+
export function encodeDidSegment(did) {
|
|
21
|
+
const raw = Buffer.from(String(did), 'utf-8');
|
|
22
|
+
if (raw.length === 0)
|
|
23
|
+
throw new EfficodeError('EFFICODE_DID_SEGMENT_BAD', '身份串为空');
|
|
24
|
+
if (raw.length > 0xffffff)
|
|
25
|
+
throw new EfficodeError('EFFICODE_DID_SEGMENT_BAD', `身份串过长 (${raw.length}B > 16MB)`);
|
|
26
|
+
const full = createHash('sha256').update(raw).digest();
|
|
27
|
+
const seg = new Uint8Array(DID_SEGMENT_BYTES);
|
|
28
|
+
seg[0] = DID_ALGO_SHA256_TRUNC28;
|
|
29
|
+
seg[1] = (raw.length >>> 16) & 0xff;
|
|
30
|
+
seg[2] = (raw.length >>> 8) & 0xff;
|
|
31
|
+
seg[3] = raw.length & 0xff;
|
|
32
|
+
seg.set(full.subarray(0, DID_SEGMENT_BYTES - 4), 4);
|
|
33
|
+
return seg;
|
|
34
|
+
}
|
|
35
|
+
/** 解: 32B 段 → 结构。长度不足 / algo 未知 → 抛 */
|
|
36
|
+
export function decodeDidSegment(seg) {
|
|
37
|
+
if (seg.length !== DID_SEGMENT_BYTES) {
|
|
38
|
+
throw new EfficodeError('EFFICODE_DID_SEGMENT_BAD', `DID 段应为 ${DID_SEGMENT_BYTES}B, 实得 ${seg.length}B`);
|
|
39
|
+
}
|
|
40
|
+
const algo = seg[0];
|
|
41
|
+
if (algo !== DID_ALGO_SHA256_TRUNC28) {
|
|
42
|
+
throw new EfficodeError('EFFICODE_DID_SEGMENT_BAD', `未知 DID 摘要算法 id=${algo}`, 0);
|
|
43
|
+
}
|
|
44
|
+
const origLen = (seg[1] << 16) | (seg[2] << 8) | seg[3];
|
|
45
|
+
return { algo, origLen, digest: seg.slice(4) };
|
|
46
|
+
}
|
|
47
|
+
/** 摘要段的稳定文本形式: `algo1:<56 hex>` —— 可安全落日志 (单向, 不可还原身份) */
|
|
48
|
+
export function didDigestString(seg) {
|
|
49
|
+
return `algo${seg.algo}:${Buffer.from(seg.digest).toString('hex')}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 校验一条身份声明是否与段匹配。
|
|
53
|
+
* 这是**明文比对** (拿原身份串再算一遍摘要), 不是签名验证 —— 返回值里如实标出方式。
|
|
54
|
+
*/
|
|
55
|
+
export function matchesDidSegment(did, seg) {
|
|
56
|
+
const expected = encodeDidSegment(did);
|
|
57
|
+
let match = expected.length === seg.length;
|
|
58
|
+
if (match) {
|
|
59
|
+
for (let i = 0; i < seg.length; i++)
|
|
60
|
+
if (expected[i] !== seg[i]) {
|
|
61
|
+
match = false;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { match, method: 'digest', signed: false };
|
|
66
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* efficode - AI 专用交流语言的**参考实现** (v0.1.0)
|
|
3
|
+
*
|
|
4
|
+
* 模块划分:
|
|
5
|
+
* types.ts 公共类型 / 失败码
|
|
6
|
+
* ops.ts 指令集与词典 (符号 ↔ 二进制操作码, 前缀表达式)
|
|
7
|
+
* crc32.ts 包尾 CRC32 (纯 JS, 任何运行面可算)
|
|
8
|
+
* lz77.ts 自解压数据块用的轻量 LZ77 (真实实现)
|
|
9
|
+
* compress.ts 可插拔压缩层 (none / lz77 / deflate)
|
|
10
|
+
* did.ts DID 验证段 (32B 摘要段)
|
|
11
|
+
* packet.ts 包封装: [Header 2B]|[DID 32B]|[指令段]|[数据块]|[CRC32 4B]
|
|
12
|
+
* negotiate.ts 智能体"用哪种语言"的声明/协商/回落与记录
|
|
13
|
+
*
|
|
14
|
+
* 状态的**唯一事实源**在 docs/wiki/efficode.md 的逐节标注 (已实现 / 规范中 / 未实现);
|
|
15
|
+
* 本目录只提供代码, 代码里的注释只写"这段代码真做了什么"。
|
|
16
|
+
*/
|
|
17
|
+
export * from './types.js';
|
|
18
|
+
export * from './ops.js';
|
|
19
|
+
export * from './crc32.js';
|
|
20
|
+
export * from './lz77.js';
|
|
21
|
+
export * from './compress.js';
|
|
22
|
+
export * from './did.js';
|
|
23
|
+
export * from './packet.js';
|
|
24
|
+
export * from './negotiate.js';
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* efficode/lz77 - 自解压数据块用的轻量 LZ77 (真实实现, 不是占位)
|
|
3
|
+
*
|
|
4
|
+
* 数据块布局 (自解压头 + 码流):
|
|
5
|
+
* [varint origLen][varint compLen][token stream]
|
|
6
|
+
*
|
|
7
|
+
* token stream = 控制字节 + 载荷, 每控制字节管 8 个 token, MSB 在前:
|
|
8
|
+
* 控制位 0 → 后接 1 字节字面量
|
|
9
|
+
* 控制位 1 → 后接 2 字节匹配 (12 位 offset, 4 位 length-3)
|
|
10
|
+
*
|
|
11
|
+
* origLen 在头里 ⇒ 解码器**不需要**猜结束位置 (靠输出长度收敛), 尾随多余码流一律报错。
|
|
12
|
+
*
|
|
13
|
+
* 匹配查找用 deflate 那套**哈希链**: 3 字节前缀哈希 → 链上回溯 (最多 MAX_CHAIN 个候选).
|
|
14
|
+
* 第一版是"从 pos-1 逐个往前比"的朴素写法, 在 `...\n` 这种周期 > 16 的重复文本上**找不到匹配**
|
|
15
|
+
* (300 行重复文本压完反而膨胀 1538B —— 实测出来的, 已换掉)。
|
|
16
|
+
*
|
|
17
|
+
* 诚实边界 (写进 wiki 时按实测数字):
|
|
18
|
+
* - 参考实现: 窗口 4095B / 最短匹配 3B / 最长匹配 18B, **没有**哈夫曼那一层;
|
|
19
|
+
* - 对重复度高的结构化文本有效, 对**随机字节**会膨胀 (控制位占 1/8);
|
|
20
|
+
* - 真实压缩率对比见 scripts/verify-efficode.ts 的真测输出, 不许在文档里编。
|
|
21
|
+
*/
|
|
22
|
+
import { EfficodeError } from './types.js';
|
|
23
|
+
const WINDOW = 4095; // 12 位 offset (0 保留为非法)
|
|
24
|
+
const MIN_MATCH = 3; // length-3 存 4 位 ⇒ 3..18
|
|
25
|
+
const MAX_MATCH = 18;
|
|
26
|
+
const MAX_CHAIN = 16; // 哈希链回溯上限 (控制最坏耗时)
|
|
27
|
+
const HASH_SIZE = 1 << 16; // 3 字节前缀哈希表大小
|
|
28
|
+
function writeVarint(out, n) {
|
|
29
|
+
let v = n >>> 0;
|
|
30
|
+
while (v >= 0x80) {
|
|
31
|
+
out.push((v & 0x7f) | 0x80);
|
|
32
|
+
v >>>= 7;
|
|
33
|
+
}
|
|
34
|
+
out.push(v & 0xff);
|
|
35
|
+
}
|
|
36
|
+
function readVarint(buf, pos) {
|
|
37
|
+
let value = 0;
|
|
38
|
+
let shift = 0;
|
|
39
|
+
let i = pos;
|
|
40
|
+
for (;;) {
|
|
41
|
+
if (i >= buf.length)
|
|
42
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', 'varint 未读完就到底', i);
|
|
43
|
+
const b = buf[i++];
|
|
44
|
+
value |= (b & 0x7f) << shift;
|
|
45
|
+
if ((b & 0x80) === 0)
|
|
46
|
+
break;
|
|
47
|
+
shift += 7;
|
|
48
|
+
if (shift > 28)
|
|
49
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', 'varint 超过 32 位上限', i);
|
|
50
|
+
}
|
|
51
|
+
return { value: value >>> 0, next: i };
|
|
52
|
+
}
|
|
53
|
+
/** 压缩。返回 [varint origLen][varint compLen][tokens] */
|
|
54
|
+
export function lz77Compress(input) {
|
|
55
|
+
const out = [];
|
|
56
|
+
writeVarint(out, input.length);
|
|
57
|
+
const bodyStart = out.length;
|
|
58
|
+
out.push(0); // 占位 (1B, varint(0)); 最后整体重拼, 不依赖占位宽度
|
|
59
|
+
const head = new Int32Array(HASH_SIZE).fill(-1);
|
|
60
|
+
const prev = new Int32Array(input.length).fill(-1);
|
|
61
|
+
const insertAt = (pos) => {
|
|
62
|
+
if (pos + MIN_MATCH > input.length)
|
|
63
|
+
return;
|
|
64
|
+
const h = hash3(input, pos);
|
|
65
|
+
prev[pos] = head[h];
|
|
66
|
+
head[h] = pos;
|
|
67
|
+
};
|
|
68
|
+
const findMatch = (pos) => {
|
|
69
|
+
if (pos + MIN_MATCH > input.length)
|
|
70
|
+
return null;
|
|
71
|
+
const h = hash3(input, pos);
|
|
72
|
+
const maxLen = Math.min(MAX_MATCH, input.length - pos);
|
|
73
|
+
let cand = head[h];
|
|
74
|
+
let chain = 0;
|
|
75
|
+
let bestLen = 0;
|
|
76
|
+
let bestOff = 0;
|
|
77
|
+
while (cand >= 0 && chain < MAX_CHAIN) {
|
|
78
|
+
const distance = pos - cand;
|
|
79
|
+
if (distance > WINDOW)
|
|
80
|
+
break;
|
|
81
|
+
let len = 0;
|
|
82
|
+
while (len < maxLen && input[cand + len] === input[pos + len])
|
|
83
|
+
len++;
|
|
84
|
+
if (len > bestLen) {
|
|
85
|
+
bestLen = len;
|
|
86
|
+
bestOff = distance;
|
|
87
|
+
if (len === maxLen)
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
cand = prev[cand];
|
|
91
|
+
chain++;
|
|
92
|
+
}
|
|
93
|
+
return bestLen >= MIN_MATCH ? { offset: bestOff, length: bestLen } : null;
|
|
94
|
+
};
|
|
95
|
+
let i = 0;
|
|
96
|
+
while (i < input.length) {
|
|
97
|
+
const controlIdx = out.length;
|
|
98
|
+
let control = 0;
|
|
99
|
+
out.push(0);
|
|
100
|
+
for (let bit = 7; bit >= 0 && i < input.length; bit--) {
|
|
101
|
+
const match = findMatch(i);
|
|
102
|
+
if (match) {
|
|
103
|
+
control |= 1 << bit;
|
|
104
|
+
const v = ((match.offset & 0xfff) << 4) | ((match.length - MIN_MATCH) & 0xf);
|
|
105
|
+
out.push((v >>> 8) & 0xff, v & 0xff);
|
|
106
|
+
for (let k = 0; k < match.length; k++)
|
|
107
|
+
insertAt(i + k);
|
|
108
|
+
i += match.length;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
out.push(input[i]);
|
|
112
|
+
insertAt(i);
|
|
113
|
+
i += 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
out[controlIdx] = control;
|
|
117
|
+
}
|
|
118
|
+
// compLen = 码流字节数 (= 占位字节之后的所有字节); 之前这里把占位字节也算进去了 ⇒ 声明比实际多 1 ⇒ 解码必报截断
|
|
119
|
+
const body = out.slice(bodyStart + 1);
|
|
120
|
+
const filled = [];
|
|
121
|
+
writeVarint(filled, input.length);
|
|
122
|
+
writeVarint(filled, body.length);
|
|
123
|
+
filled.push(...body);
|
|
124
|
+
return Uint8Array.from(filled);
|
|
125
|
+
}
|
|
126
|
+
function hash3(b, i) {
|
|
127
|
+
return (((b[i] << 10) ^ (b[i + 1] << 5) ^ b[i + 2]) & (HASH_SIZE - 1)) >>> 0;
|
|
128
|
+
}
|
|
129
|
+
/** 解压 (严格)。码流不足 / 长度对不上 / 超长一律抛, 不做任何"尽量解" */
|
|
130
|
+
export function lz77Decompress(block) {
|
|
131
|
+
const head = readVarint(block, 0);
|
|
132
|
+
const origLen = head.value;
|
|
133
|
+
const head2 = readVarint(block, head.next);
|
|
134
|
+
const compLen = head2.value;
|
|
135
|
+
const bodyEnd = head2.next + compLen;
|
|
136
|
+
if (bodyEnd > block.length) {
|
|
137
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', `LZ77 码流声明 ${compLen}B 但只剩 ${block.length - head2.next}B`, head2.next);
|
|
138
|
+
}
|
|
139
|
+
const out = [];
|
|
140
|
+
let pos = head2.next;
|
|
141
|
+
while (out.length < origLen) {
|
|
142
|
+
if (pos >= bodyEnd) {
|
|
143
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', `LZ77 码流提前结束 (期望 ${origLen}B, 实得 ${out.length}B)`, pos);
|
|
144
|
+
}
|
|
145
|
+
const control = block[pos++];
|
|
146
|
+
for (let bit = 7; bit >= 0 && out.length < origLen; bit--) {
|
|
147
|
+
if (control & (1 << bit)) {
|
|
148
|
+
if (pos + 2 > bodyEnd) {
|
|
149
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', 'LZ77 匹配 token 缺字节', pos);
|
|
150
|
+
}
|
|
151
|
+
const v = (block[pos] << 8) | block[pos + 1];
|
|
152
|
+
pos += 2;
|
|
153
|
+
const offset = (v >>> 4) & 0xfff;
|
|
154
|
+
const length = (v & 0xf) + MIN_MATCH;
|
|
155
|
+
if (offset === 0 || offset > out.length) {
|
|
156
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', `LZ77 匹配 offset=${offset} 越界 (已解 ${out.length}B)`, pos);
|
|
157
|
+
}
|
|
158
|
+
for (let k = 0; k < length; k++)
|
|
159
|
+
out.push(out[out.length - offset]);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
if (pos >= bodyEnd) {
|
|
163
|
+
throw new EfficodeError('EFFICODE_TRUNCATED', 'LZ77 字面量 token 缺字节', pos);
|
|
164
|
+
}
|
|
165
|
+
out.push(block[pos++]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return Uint8Array.from(out);
|
|
170
|
+
}
|