evolcore 0.0.1 → 0.0.3
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 +64 -0
- package/LICENSE +21 -0
- package/MIGRATION-0.5.0.md +378 -0
- package/README.md +318 -14
- package/ROLE_ACCESS_CONTROL.md +174 -0
- package/assets/.env.template +4 -0
- package/bin/ec-safe-output.js +161 -0
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent.js +163 -0
- package/dist/agents/claude-runner.js +2565 -0
- package/dist/agents/codex-app-server-client.js +448 -0
- package/dist/agents/codex-runner.js +2682 -0
- package/dist/agents/gemini-runner.js +666 -0
- package/dist/agents/runner-types.js +75 -0
- package/dist/aun/aid/agentmd.js +216 -0
- package/dist/aun/aid/client.js +132 -0
- package/dist/aun/aid/control-aid.js +91 -0
- package/dist/aun/aid/identity.js +518 -0
- package/dist/aun/aid/index.js +4 -0
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/group-identity.js +10 -0
- package/dist/aun/msg/group-index.js +6 -0
- package/dist/aun/msg/group.js +1231 -0
- package/dist/aun/msg/history.js +123 -0
- package/dist/aun/msg/index.js +5 -0
- package/dist/aun/msg/p2p.js +393 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +137 -0
- package/dist/aun/outbox.js +168 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +25 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/service-proxy.js +225 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +68 -0
- package/dist/channels/aun.js +4164 -0
- package/dist/channels/contact-bind-code.js +134 -0
- package/dist/channels/daemon.js +422 -0
- package/dist/channels/dingtalk.js +1479 -0
- package/dist/channels/feishu.js +1865 -0
- package/dist/channels/qqbot.js +409 -0
- package/dist/channels/wechat.js +817 -0
- package/dist/channels/wecom-card.js +101 -0
- package/dist/channels/wecom-onboarding.js +82 -0
- package/dist/channels/wecom-state.js +191 -0
- package/dist/channels/wecom.js +1157 -0
- package/dist/cli/agent-command.js +642 -0
- package/dist/cli/agent.js +1059 -0
- package/dist/cli/aun-commands.js +2003 -0
- package/dist/cli/bench.js +1228 -0
- package/dist/cli/cli-argv.js +66 -0
- package/dist/cli/code-stats.js +329 -0
- package/dist/cli/command-log.js +82 -0
- package/dist/cli/config-selector.js +69 -0
- package/dist/cli/config.js +261 -0
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +62 -0
- package/dist/cli/daemon-commands.js +2750 -0
- package/dist/cli/fs-command.js +1447 -0
- package/dist/cli/handoff-command.js +302 -0
- package/dist/cli/help.js +35 -0
- package/dist/cli/index.js +374 -0
- package/dist/cli/init-channel.js +1372 -0
- package/dist/cli/init.js +590 -0
- package/dist/cli/link-rules.js +240 -0
- package/dist/cli/model.js +591 -0
- package/dist/cli/net-check.js +723 -0
- package/dist/cli/queue-command.js +150 -0
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +344 -0
- package/dist/cli/restart-monitor.js +480 -0
- package/dist/cli/stats.js +609 -0
- package/dist/cli/task-context.js +80 -0
- package/dist/cli/trigger-command.js +545 -0
- package/dist/cli/version.js +93 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +673 -0
- package/dist/config/boot-log.js +266 -0
- package/dist/config/builtin-role-templates.js +42 -0
- package/dist/config/builtin-roles.js +91 -0
- package/dist/config/config-batch-get.js +11 -0
- package/dist/config/config-field-policy.js +261 -0
- package/dist/config/config-manager.js +1120 -0
- package/dist/config/config-operation-service.js +384 -0
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-book-store.js +454 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +224 -0
- package/dist/config/contact-operation-service.js +110 -0
- package/dist/config/gateway-config.js +858 -0
- package/dist/config/lifecycle.js +17 -0
- package/dist/config/mention-mode.js +27 -0
- package/dist/config/merge.js +161 -0
- package/dist/config/owner-policy.js +4 -0
- package/dist/config/peer-role-resolver.js +218 -0
- package/dist/config/resolved-config-op.js +483 -0
- package/dist/config/role-config-v4-startup.js +32 -0
- package/dist/config/role-config-v5-startup.js +27 -0
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-schema.js +105 -0
- package/dist/config/role-service.js +156 -0
- package/dist/config/role-store.js +215 -0
- package/dist/config/roles.js +64 -0
- package/dist/config/schema-registry.js +154 -0
- package/dist/config/snapshot.js +598 -0
- package/dist/config-store.js +501 -0
- package/dist/core/auth/agent-delegation.js +111 -0
- package/dist/core/auth/auth-gateway.js +166 -0
- package/dist/core/auth/authenticated-actor.js +6 -0
- package/dist/core/auth/authorization-audit.js +119 -0
- package/dist/core/auth/operation-authorizer.js +720 -0
- package/dist/core/auth/operation-catalog.js +731 -0
- package/dist/core/baseagent-loader.js +54 -0
- package/dist/core/bootstrap-service.js +175 -0
- package/dist/core/capability/capability-manager.js +316 -0
- package/dist/core/capability/providers/claude-capability-provider.js +176 -0
- package/dist/core/capability/providers/codex-capability-provider.js +148 -0
- package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
- package/dist/core/capability/types.js +27 -0
- package/dist/core/causation/audit.js +103 -0
- package/dist/core/causation/aun-association.js +111 -0
- package/dist/core/causation/context.js +93 -0
- package/dist/core/causation/index.js +4 -0
- package/dist/core/causation/types.js +2 -0
- package/dist/core/channel-loader.js +277 -0
- package/dist/core/command/agent-control.js +616 -0
- package/dist/core/command/cli-intent-parser.js +225 -0
- package/dist/core/command/command-handler.js +1733 -0
- package/dist/core/command/connect-menu.js +374 -0
- package/dist/core/command/menu-handler.js +3452 -0
- package/dist/core/command/menu-protocol.js +247 -0
- package/dist/core/command/role-menu.js +1623 -0
- package/dist/core/command/slash-gate.js +148 -0
- package/dist/core/command/slash-handler.js +3066 -0
- package/dist/core/daemon-file-cache.js +222 -0
- package/dist/core/event-bus.js +32 -0
- package/dist/core/event-catalog.js +810 -0
- package/dist/core/evolagent-registry.js +545 -0
- package/dist/core/evolagent.js +342 -0
- package/dist/core/handoff/dispatcher.js +229 -0
- package/dist/core/handoff/mutex.js +46 -0
- package/dist/core/handoff/runtime.js +324 -0
- package/dist/core/handoff/store.js +537 -0
- package/dist/core/handoff/types.js +12 -0
- package/dist/core/inference/text-inference.js +173 -0
- package/dist/core/interaction-registration.js +10 -0
- package/dist/core/interaction-router.js +278 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/im-renderer.js +659 -0
- package/dist/core/message/items-formatter.js +76 -0
- package/dist/core/message/logical-queue-bridge.js +123 -0
- package/dist/core/message/message-bridge.js +874 -0
- package/dist/core/message/message-cache.js +56 -0
- package/dist/core/message/message-log.js +339 -0
- package/dist/core/message/message-processor.js +4 -0
- package/dist/core/message/message-queue.js +1446 -0
- package/dist/core/message/message-utils.js +76 -0
- package/dist/core/message/peer-mode.js +105 -0
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +33 -0
- package/dist/core/message/response-engine.js +4020 -0
- package/dist/core/message/response-snapshot.js +83 -0
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +139 -0
- package/dist/core/message/stream-idle-monitor.js +124 -0
- package/dist/core/model/config-scope.js +162 -0
- package/dist/core/model/field-scope.js +78 -0
- package/dist/core/model/model-catalog.js +227 -0
- package/dist/core/model/model-diagnostics.js +182 -0
- package/dist/core/model/model-permission.js +90 -0
- package/dist/core/permission/approval-gateway.js +1017 -0
- package/dist/core/permission/ec-command-parser.js +347 -0
- package/dist/core/permission/execution-sandbox.js +16 -0
- package/dist/core/permission/index.js +6 -0
- package/dist/core/permission/mode.js +24 -0
- package/dist/core/permission/sandbox-runtime.js +265 -0
- package/dist/core/permission/tool-policy.js +1019 -0
- package/dist/core/permission/unix-socket-policy.js +99 -0
- package/dist/core/protected-paths.js +332 -0
- package/dist/core/relation/peer-identity.js +222 -0
- package/dist/core/relation/peer-key.js +1 -0
- package/dist/core/role/runtime-policy.js +141 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-fs-store.js +273 -0
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +1643 -0
- package/dist/core/session/session-mapper.js +100 -0
- package/dist/core/session/session-renew.js +314 -0
- package/dist/core/session/session-title.js +128 -0
- package/dist/core/session/session-turn-coordinator.js +205 -0
- package/dist/core/session/session-turns.js +67 -0
- package/dist/core/system-channels.js +29 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/group-rules-sync.js +345 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/kit-renderer.js +359 -0
- package/dist/eck/manifest-engine.js +446 -0
- package/dist/eck/message-renderer.js +199 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +2926 -4
- package/dist/ipc.js +777 -0
- package/dist/paths.js +262 -0
- package/dist/product.js +18 -0
- package/dist/response-system/context-builder.js +71 -0
- package/dist/response-system/coordinator.js +117 -0
- package/dist/response-system/decision-executor.js +86 -0
- package/dist/response-system/engines/v1/index.js +21 -0
- package/dist/response-system/engines/v1/interactive-flow.js +27 -0
- package/dist/response-system/engines/v1/proactive-flow.js +137 -0
- package/dist/response-system/engines/v1/types.js +1 -0
- package/dist/response-system/extensions.js +41 -0
- package/dist/response-system/index.js +6 -0
- package/dist/response-system/modes/index.js +7 -0
- package/dist/response-system/modes/single-session/index.js +72 -0
- package/dist/response-system/queues/fifo-queue.js +44 -0
- package/dist/response-system/queues/index.js +6 -0
- package/dist/response-system/queues/lifo-queue.js +42 -0
- package/dist/response-system/queues/priority-queue.js +63 -0
- package/dist/response-system/registry.js +97 -0
- package/dist/response-system/resolver.js +37 -0
- package/dist/response-system/selector.js +23 -0
- package/dist/response-system/types.js +7 -0
- package/dist/stats/billing.js +163 -0
- package/dist/stats/budget.js +93 -0
- package/dist/stats/db.js +403 -0
- package/dist/stats/eck-vars.js +89 -0
- package/dist/stats/index.js +11 -0
- package/dist/stats/normalizer.js +80 -0
- package/dist/stats/price-resolver.js +138 -0
- package/dist/stats/query.js +763 -0
- package/dist/stats/role-budget.js +168 -0
- package/dist/stats/writer.js +151 -0
- package/dist/trigger/anomaly-store.js +258 -0
- package/dist/trigger/audit.js +152 -0
- package/dist/trigger/event-source.js +119 -0
- package/dist/trigger/feedback.js +685 -0
- package/dist/trigger/history.js +290 -0
- package/dist/trigger/manager.js +294 -0
- package/dist/trigger/parser.js +595 -0
- package/dist/trigger/patch.js +155 -0
- package/dist/trigger/scheduler.js +1602 -0
- package/dist/trigger/script-executor.js +155 -0
- package/dist/trigger/state.js +145 -0
- package/dist/trigger/types.js +1 -0
- package/dist/trigger/validation.js +634 -0
- package/dist/types.js +12 -0
- package/dist/utils/aid-bind.js +313 -0
- package/dist/utils/atomic-write.js +95 -0
- package/dist/utils/avatar-upload.js +123 -0
- package/dist/utils/cross-platform.js +297 -0
- package/dist/utils/ecweb-utils.js +73 -0
- package/dist/utils/error-dict.json +153 -0
- package/dist/utils/error-utils.js +349 -0
- package/dist/utils/instance-registry.js +444 -0
- package/dist/utils/locale.js +21 -0
- package/dist/utils/log-writer.js +270 -0
- package/dist/utils/logger.js +89 -0
- package/dist/utils/markdown-to-plain-text.js +20 -0
- package/dist/utils/media-cache.js +274 -0
- package/dist/utils/model-prices.jsonl +20 -0
- package/dist/utils/npm-ops.js +210 -0
- package/dist/utils/process-introspect.js +133 -0
- package/dist/utils/process-tree-stats.js +271 -0
- package/dist/utils/project-path.js +74 -0
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stats.js +410 -0
- package/dist/utils/system-memory.js +62 -0
- package/dist/utils/tool-summary.js +284 -0
- package/dist/utils/welcome.js +268 -0
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +66 -0
- package/kits/docs/aun/CHEATSHEET.md +19 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +65 -0
- package/kits/docs/channels/feishu.md +56 -0
- package/kits/docs/context-assembly.md +366 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolcore/INDEX.md +68 -0
- package/kits/docs/evolcore/agent.md +77 -0
- package/kits/docs/evolcore/aid.md +52 -0
- package/kits/docs/evolcore/config.md +149 -0
- package/kits/docs/evolcore/contact.md +57 -0
- package/kits/docs/evolcore/ctl.md +46 -0
- package/kits/docs/evolcore/event.md +216 -0
- package/kits/docs/evolcore/fs-architecture.md +1215 -0
- package/kits/docs/evolcore/fs.md +110 -0
- package/kits/docs/evolcore/group-fs.md +17 -0
- package/kits/docs/evolcore/group-rules.md +226 -0
- package/kits/docs/evolcore/group.md +150 -0
- package/kits/docs/evolcore/model.md +50 -0
- package/kits/docs/evolcore/msg.md +136 -0
- package/kits/docs/evolcore/response.md +75 -0
- package/kits/docs/evolcore/rpc.md +37 -0
- package/kits/docs/evolcore/self-summary.md +29 -0
- package/kits/docs/evolcore/stats.md +83 -0
- package/kits/docs/evolcore/storage.md +50 -0
- package/kits/docs/evolcore/trigger.md +539 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +23 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/docs/prompt-loading-architecture.md +266 -0
- package/kits/docs/venues/aun-group.md +45 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +25 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.auxiliary.json +43 -0
- package/kits/eck_manifest.json +203 -0
- package/kits/eck_message_manifest.json +63 -0
- package/kits/migrations/README-role-config-v4.md +32 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
- package/kits/migrations/migrate-role-config-v4.mjs +623 -0
- package/kits/migrations/migrate-role-config-v5.mjs +346 -0
- package/kits/migrations/rename-config-file.mjs +99 -0
- package/kits/rules/01-overview.md +142 -0
- package/kits/rules/02-navigation.md +76 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +59 -0
- package/kits/rules/05-venue.md +44 -0
- package/kits/rules/06-channel.md +59 -0
- package/kits/schemas/_meta.json +32 -0
- package/kits/schemas/agent-config.schema.1.json +177 -0
- package/kits/schemas/agent-config.schema.2.json +239 -0
- package/kits/schemas/agent-config.schema.3.json +119 -0
- package/kits/schemas/agent-config.schema.4.json +208 -0
- package/kits/schemas/agent-config.schema.5.json +326 -0
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/contact-book.schema.1.json +36 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/daemon.schema.1.json +90 -0
- package/kits/schemas/defaults.schema.1.json +81 -0
- package/kits/schemas/menu-exec-schema-commands.md +208 -0
- package/kits/schemas/migrations/README.md +28 -0
- package/kits/schemas/relation-config.schema.1.json +158 -0
- package/kits/schemas/relation-config.schema.2.json +73 -0
- package/kits/schemas/relation-config.schema.3.json +50 -0
- package/kits/schemas/relation-config.schema.4.json +47 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/role-config.schema.1.json +201 -0
- package/kits/schemas/role-registry.schema.1.json +35 -0
- package/kits/schemas/single-session.schema.1.json +31 -0
- package/kits/templates/bootstrap-welcome.md +15 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
- package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/message-fragments/item.md +2 -0
- package/kits/templates/roles/admin.json +9 -0
- package/kits/templates/roles/member.json +41 -0
- package/kits/templates/roles/owner.json +9 -0
- package/kits/templates/roles/visitor.json +40 -0
- package/kits/templates/system-fragments/baseagent.md +14 -0
- package/kits/templates/system-fragments/bootstrap.md +16 -0
- package/kits/templates/system-fragments/channel.md +48 -0
- package/kits/templates/system-fragments/commands.md +28 -0
- package/kits/templates/system-fragments/identity.md +11 -0
- package/kits/templates/system-fragments/relation.md +19 -0
- package/kits/templates/system-fragments/session.md +53 -0
- package/kits/templates/system-fragments/venue.md +31 -0
- package/package.json +50 -15
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LogWriter — 统一的日志文件管理。
|
|
3
|
+
*
|
|
4
|
+
* 一个实例 = 一份"活跃日志 + 历史归档"。所有 EvolCore 内部需要写日志的地方
|
|
5
|
+
* 都应通过它,避免每个模块各自实现切片/清理。
|
|
6
|
+
*
|
|
7
|
+
* 切片规则:
|
|
8
|
+
* - 'hourly' / 'daily':进入新时段时把当前活跃文件 rename 成
|
|
9
|
+
* `<base>-YYYYMMDD-HH.log` / `<base>-YYYYMMDD.log`,活跃文件名固定为
|
|
10
|
+
* `<base>.log`。这样 `tail -F <base>.log` 能跨切片续接(必须 -F,跟 path)。
|
|
11
|
+
* - 'size':活跃文件超过 maxSize 时 rename 成 `<base>.log.<ISO-15>`。
|
|
12
|
+
* - 'none':不切。
|
|
13
|
+
*
|
|
14
|
+
* 清理:进入新时段或启动时按 retention 删除过旧的归档文件。
|
|
15
|
+
*
|
|
16
|
+
* 设计取舍:LogWriter 不持有 stream,每次 write 时按需 open-append-close。
|
|
17
|
+
* 优点:跨切片简单(rename 后下次 write 自动开新文件);
|
|
18
|
+
* 代价:每行一次 open/close 在高频日志下不便宜——但 EvolCore 的日志量
|
|
19
|
+
* 远低于 OS 缓存带宽,实测无瓶颈。
|
|
20
|
+
* 后续若有热点(如 messages.log)可改成持流模式 + 切片时 close。
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'fs';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import { clearInterval as clearNativeInterval, clearTimeout as clearNativeTimeout, setInterval as setNativeInterval, setTimeout as setNativeTimeout, } from 'node:timers';
|
|
25
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
26
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
27
|
+
/**
|
|
28
|
+
* Rotate the daemon's redirected stdout before a launcher opens it for append.
|
|
29
|
+
* stdout is owned by the launcher rather than LogWriter, so both start and
|
|
30
|
+
* restart paths must call this after the previous daemon has exited.
|
|
31
|
+
*/
|
|
32
|
+
export function rotateStdoutLog(logDir, options = {}) {
|
|
33
|
+
const maxSize = options.maxSize ?? 10 * 1024 * 1024;
|
|
34
|
+
const retentionMs = options.retentionMs ?? 7 * DAY_MS;
|
|
35
|
+
const now = options.now ?? Date.now();
|
|
36
|
+
const stdoutLog = path.join(logDir, 'stdout.log');
|
|
37
|
+
let rotatedPath = null;
|
|
38
|
+
try {
|
|
39
|
+
if (fs.statSync(stdoutLog).size > maxSize) {
|
|
40
|
+
const timestamp = new Date(now).toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
41
|
+
const baseArchive = `${stdoutLog}.${timestamp}`;
|
|
42
|
+
let archive = baseArchive;
|
|
43
|
+
let suffix = 1;
|
|
44
|
+
while (fs.existsSync(archive))
|
|
45
|
+
archive = `${baseArchive}.${suffix++}`;
|
|
46
|
+
fs.renameSync(stdoutLog, archive);
|
|
47
|
+
rotatedPath = archive;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Missing/unreadable stdout must not prevent the daemon from starting.
|
|
52
|
+
}
|
|
53
|
+
const removedArchives = [];
|
|
54
|
+
const cutoff = now - retentionMs;
|
|
55
|
+
try {
|
|
56
|
+
for (const name of fs.readdirSync(logDir)) {
|
|
57
|
+
if (!name.startsWith('stdout.log.'))
|
|
58
|
+
continue;
|
|
59
|
+
const full = path.join(logDir, name);
|
|
60
|
+
try {
|
|
61
|
+
const stat = fs.lstatSync(full);
|
|
62
|
+
if (stat.isSymbolicLink() || stat.mtimeMs < cutoff) {
|
|
63
|
+
fs.unlinkSync(full);
|
|
64
|
+
removedArchives.push(full);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
return { rotatedPath, removedArchives };
|
|
72
|
+
}
|
|
73
|
+
export class LogWriter {
|
|
74
|
+
baseName;
|
|
75
|
+
logDir;
|
|
76
|
+
rotation;
|
|
77
|
+
maxSize;
|
|
78
|
+
retentionMs;
|
|
79
|
+
/** 当前活跃时段 tag(hourly: YYYYMMDD-HH,daily: YYYYMMDD),用于检测切片 */
|
|
80
|
+
currentTag;
|
|
81
|
+
/** 周期清理 timer(unref 不阻塞退出) */
|
|
82
|
+
cleanupTimer = null;
|
|
83
|
+
boundaryTimer = null;
|
|
84
|
+
constructor(opts) {
|
|
85
|
+
this.baseName = opts.baseName;
|
|
86
|
+
this.logDir = opts.logDir;
|
|
87
|
+
this.rotation = opts.rotation;
|
|
88
|
+
this.maxSize = opts.maxSize ?? 10 * 1024 * 1024;
|
|
89
|
+
this.retentionMs =
|
|
90
|
+
(opts.retention.hours ?? 0) * HOUR_MS +
|
|
91
|
+
(opts.retention.days ?? 0) * DAY_MS;
|
|
92
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
93
|
+
this.currentTag = this.tagOfNow();
|
|
94
|
+
// 启动时如果 active 文件已存在但归属上一时段,先归档掉
|
|
95
|
+
this.rotateIfNeeded(/* sync */ true);
|
|
96
|
+
this.cleanupOldArchives();
|
|
97
|
+
// 周期触发切片+清理。两段式定时器:先用 setTimeout 对齐到下一个整点
|
|
98
|
+
//(hourly: 下个整点;daily: 明日 00:00),然后切到每小时一次的 setInterval。
|
|
99
|
+
// 这样空闲期也能按时切片——而不是等到下一次有人 write 才补切。
|
|
100
|
+
if (this.rotation === 'hourly' || this.rotation === 'daily') {
|
|
101
|
+
const tick = () => {
|
|
102
|
+
this.rotateIfNeeded(false);
|
|
103
|
+
this.cleanupOldArchives();
|
|
104
|
+
};
|
|
105
|
+
const initialDelay = this.msUntilNextBoundary();
|
|
106
|
+
this.boundaryTimer = setNativeTimeout(() => {
|
|
107
|
+
this.boundaryTimer = null;
|
|
108
|
+
tick();
|
|
109
|
+
this.cleanupTimer = setNativeInterval(tick, HOUR_MS);
|
|
110
|
+
this.cleanupTimer.unref?.();
|
|
111
|
+
}, initialDelay);
|
|
112
|
+
this.boundaryTimer.unref?.();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** 写一行(自动加 \n) */
|
|
116
|
+
write(line) {
|
|
117
|
+
this.rotateIfNeeded(false);
|
|
118
|
+
try {
|
|
119
|
+
fs.appendFileSync(this.activePath(), line + '\n');
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// 写入失败不抛——日志不能影响业务
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** 关闭(停掉清理 timer)。一般不需要调用,进程退出 timer 自动失效 */
|
|
126
|
+
close() {
|
|
127
|
+
if (this.boundaryTimer) {
|
|
128
|
+
clearNativeTimeout(this.boundaryTimer);
|
|
129
|
+
this.boundaryTimer = null;
|
|
130
|
+
}
|
|
131
|
+
if (this.cleanupTimer) {
|
|
132
|
+
clearNativeInterval(this.cleanupTimer);
|
|
133
|
+
this.cleanupTimer = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** 当前活跃文件绝对路径 */
|
|
137
|
+
activePath() {
|
|
138
|
+
return path.join(this.logDir, `${this.baseName}.log`);
|
|
139
|
+
}
|
|
140
|
+
// ── Internal ──
|
|
141
|
+
/** 距离下一个整时段边界的毫秒数(hourly: 下个整点;daily: 明日 00:00) */
|
|
142
|
+
msUntilNextBoundary() {
|
|
143
|
+
const now = new Date();
|
|
144
|
+
const next = new Date(now);
|
|
145
|
+
if (this.rotation === 'daily') {
|
|
146
|
+
next.setDate(next.getDate() + 1);
|
|
147
|
+
next.setHours(0, 0, 0, 0);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
next.setHours(next.getHours() + 1, 0, 0, 0);
|
|
151
|
+
}
|
|
152
|
+
// 给 1 秒余量,确保 Date 时钟跨越整点后再触发,避免边界误差
|
|
153
|
+
return Math.max(1000, next.getTime() - now.getTime() + 1000);
|
|
154
|
+
}
|
|
155
|
+
tagOfNow() {
|
|
156
|
+
const d = new Date();
|
|
157
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
158
|
+
const day = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
|
159
|
+
if (this.rotation === 'daily')
|
|
160
|
+
return day;
|
|
161
|
+
return `${day}-${pad(d.getHours())}`;
|
|
162
|
+
}
|
|
163
|
+
archivePath(tag) {
|
|
164
|
+
return path.join(this.logDir, `${this.baseName}-${tag}.log`);
|
|
165
|
+
}
|
|
166
|
+
rotateIfNeeded(initial) {
|
|
167
|
+
const active = this.activePath();
|
|
168
|
+
if (this.rotation === 'none')
|
|
169
|
+
return;
|
|
170
|
+
if (this.rotation === 'size') {
|
|
171
|
+
let stat;
|
|
172
|
+
try {
|
|
173
|
+
stat = fs.statSync(active);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (stat.size <= this.maxSize)
|
|
179
|
+
return;
|
|
180
|
+
const tag = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
181
|
+
try {
|
|
182
|
+
fs.renameSync(active, `${active}.${tag}`);
|
|
183
|
+
}
|
|
184
|
+
catch { }
|
|
185
|
+
this.cleanupOldArchives();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// hourly / daily
|
|
189
|
+
const nowTag = this.tagOfNow();
|
|
190
|
+
// 文件不存在 → 直接更新 currentTag,下次 write 会创建
|
|
191
|
+
let stat = null;
|
|
192
|
+
try {
|
|
193
|
+
stat = fs.statSync(active);
|
|
194
|
+
}
|
|
195
|
+
catch { /* not exist */ }
|
|
196
|
+
if (!stat) {
|
|
197
|
+
this.currentTag = nowTag;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// 启动时:用 mtime 判断 active 文件归属哪个 tag
|
|
201
|
+
const ownerTag = initial ? this.tagOfTime(stat.mtimeMs) : this.currentTag;
|
|
202
|
+
if (ownerTag === nowTag) {
|
|
203
|
+
this.currentTag = nowTag;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// 切片:把 active 重命名为 archive
|
|
207
|
+
const archive = this.archivePath(ownerTag);
|
|
208
|
+
try {
|
|
209
|
+
// 如果 archive 已存在(重名),先合并:append 老 active 内容到 archive 末尾
|
|
210
|
+
if (fs.existsSync(archive)) {
|
|
211
|
+
try {
|
|
212
|
+
const content = fs.readFileSync(active);
|
|
213
|
+
fs.appendFileSync(archive, content);
|
|
214
|
+
fs.unlinkSync(active);
|
|
215
|
+
}
|
|
216
|
+
catch { }
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
fs.renameSync(active, archive);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch { }
|
|
223
|
+
this.currentTag = nowTag;
|
|
224
|
+
}
|
|
225
|
+
tagOfTime(ms) {
|
|
226
|
+
const d = new Date(ms);
|
|
227
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
228
|
+
const day = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
|
229
|
+
if (this.rotation === 'daily')
|
|
230
|
+
return day;
|
|
231
|
+
return `${day}-${pad(d.getHours())}`;
|
|
232
|
+
}
|
|
233
|
+
cleanupOldArchives() {
|
|
234
|
+
LogWriter.cleanupArchivesIn(this.logDir, this.retentionMs);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* 全局清理:扫整个 logDir,按 retention 删除所有匹配 LogWriter 归档命名约定的文件。
|
|
238
|
+
*
|
|
239
|
+
* 命名约定:`<baseName>-YYYYMMDD-HH.log`(hourly)或 `<baseName>-YYYYMMDD.log`(daily),
|
|
240
|
+
* 其中 baseName 由字母/数字/连字符组成。
|
|
241
|
+
*
|
|
242
|
+
* 这条规则跨 baseName 统一——只要文件按这个 pattern 命名就认为受 LogWriter 体系管辖。
|
|
243
|
+
* 这样 conditional 启用的 LogWriter(如 aun trace 关闭时)不会留下永久无人清的归档:
|
|
244
|
+
* 任意 LogWriter 实例化都会顺便清掉它们。
|
|
245
|
+
*/
|
|
246
|
+
static cleanupArchivesIn(logDir, retentionMs) {
|
|
247
|
+
if (retentionMs <= 0)
|
|
248
|
+
return;
|
|
249
|
+
const cutoff = Date.now() - retentionMs;
|
|
250
|
+
let entries;
|
|
251
|
+
try {
|
|
252
|
+
entries = fs.readdirSync(logDir);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const generalPattern = /^[A-Za-z0-9][A-Za-z0-9_-]*-\d{8}(?:-\d{2})?\.log$/;
|
|
258
|
+
for (const name of entries) {
|
|
259
|
+
if (!generalPattern.test(name))
|
|
260
|
+
continue;
|
|
261
|
+
const full = path.join(logDir, name);
|
|
262
|
+
try {
|
|
263
|
+
const st = fs.lstatSync(full);
|
|
264
|
+
if (st.isSymbolicLink() || st.mtimeMs < cutoff)
|
|
265
|
+
fs.unlinkSync(full);
|
|
266
|
+
}
|
|
267
|
+
catch { }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { resolvePaths } from '../paths.js';
|
|
3
|
+
import { LogWriter } from './log-writer.js';
|
|
4
|
+
let currentLevel = process.env.LOG_LEVEL || 'INFO';
|
|
5
|
+
const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
|
|
6
|
+
const config = {
|
|
7
|
+
messageLog: process.env.MESSAGE_LOG === 'true',
|
|
8
|
+
eventLog: process.env.EVENT_LOG === 'true'
|
|
9
|
+
};
|
|
10
|
+
let writers = null;
|
|
11
|
+
fs.mkdirSync(resolvePaths().logs, { recursive: true });
|
|
12
|
+
function closeWriters(current) {
|
|
13
|
+
current.main.close();
|
|
14
|
+
current.message?.close();
|
|
15
|
+
current.event?.close();
|
|
16
|
+
current.channelIn.close();
|
|
17
|
+
current.channelOut.close();
|
|
18
|
+
}
|
|
19
|
+
function getWriters() {
|
|
20
|
+
const logDir = resolvePaths().logs;
|
|
21
|
+
if (writers?.logDir === logDir)
|
|
22
|
+
return writers;
|
|
23
|
+
if (writers)
|
|
24
|
+
closeWriters(writers);
|
|
25
|
+
writers = {
|
|
26
|
+
logDir,
|
|
27
|
+
main: new LogWriter({ baseName: 'daemon', logDir, rotation: 'hourly', retention: { hours: 24 } }),
|
|
28
|
+
message: config.messageLog
|
|
29
|
+
? new LogWriter({ baseName: 'messages', logDir, rotation: 'hourly', retention: { hours: 24 } })
|
|
30
|
+
: null,
|
|
31
|
+
event: config.eventLog
|
|
32
|
+
? new LogWriter({ baseName: 'events', logDir, rotation: 'hourly', retention: { hours: 24 } })
|
|
33
|
+
: null,
|
|
34
|
+
channelIn: new LogWriter({ baseName: 'channel-in', logDir, rotation: 'hourly', retention: { hours: 24 } }),
|
|
35
|
+
channelOut: new LogWriter({ baseName: 'channel-out', logDir, rotation: 'hourly', retention: { hours: 24 } }),
|
|
36
|
+
};
|
|
37
|
+
return writers;
|
|
38
|
+
}
|
|
39
|
+
function shouldLog(level) {
|
|
40
|
+
return (LEVELS[level] ?? 1) >= (LEVELS[currentLevel] ?? 1);
|
|
41
|
+
}
|
|
42
|
+
export function localTimestamp() {
|
|
43
|
+
const d = new Date();
|
|
44
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
45
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
46
|
+
}
|
|
47
|
+
function log(level, ...args) {
|
|
48
|
+
if (!shouldLog(level))
|
|
49
|
+
return;
|
|
50
|
+
const msg = `[${localTimestamp()}] [${level}] ${args.join(' ')}`;
|
|
51
|
+
getWriters().main.write(msg);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 设置日志级别(config 加载后调用,覆盖环境变量默认值)
|
|
55
|
+
* 优先级:config.debug.logLevel → LOG_LEVEL 环境变量 → 'INFO'
|
|
56
|
+
*/
|
|
57
|
+
export function setLogLevel(level) {
|
|
58
|
+
const upper = level.toUpperCase();
|
|
59
|
+
if (upper in LEVELS) {
|
|
60
|
+
currentLevel = upper;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function getLogLevel() {
|
|
64
|
+
return currentLevel;
|
|
65
|
+
}
|
|
66
|
+
export const logger = {
|
|
67
|
+
debug: (...args) => log('DEBUG', ...args),
|
|
68
|
+
info: (...args) => log('INFO', ...args),
|
|
69
|
+
warn: (...args) => log('WARN', ...args),
|
|
70
|
+
error: (...args) => log('ERROR', ...args),
|
|
71
|
+
message: (data) => {
|
|
72
|
+
const writer = getWriters().message;
|
|
73
|
+
if (!writer)
|
|
74
|
+
return;
|
|
75
|
+
writer.write(JSON.stringify({ ts: localTimestamp(), ...data }));
|
|
76
|
+
},
|
|
77
|
+
event: (data) => {
|
|
78
|
+
const writer = getWriters().event;
|
|
79
|
+
if (!writer)
|
|
80
|
+
return;
|
|
81
|
+
writer.write(JSON.stringify({ ts: localTimestamp(), ...data }));
|
|
82
|
+
},
|
|
83
|
+
channelIn: (data) => {
|
|
84
|
+
getWriters().channelIn.write(JSON.stringify({ ts: localTimestamp(), ...data }));
|
|
85
|
+
},
|
|
86
|
+
channelOut: (data) => {
|
|
87
|
+
getWriters().channelOut.write(JSON.stringify({ ts: localTimestamp(), ...data }));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function markdownToPlainText(text) {
|
|
2
|
+
let result = text;
|
|
3
|
+
result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code) => code.trim());
|
|
4
|
+
result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, '');
|
|
5
|
+
result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1');
|
|
6
|
+
result = result.replace(/^\|[\s:|-]+\|$/gm, '');
|
|
7
|
+
result = result.replace(/^\|(.+)\|$/gm, (_, inner) => inner.split('|').map(cell => cell.trim()).join(' '));
|
|
8
|
+
result = result.replace(/\*\*(.+?)\*\*/g, '$1');
|
|
9
|
+
result = result.replace(/\*(.+?)\*/g, '$1');
|
|
10
|
+
result = result.replace(/__(.+?)__/g, '$1');
|
|
11
|
+
result = result.replace(/_(.+?)_/g, '$1');
|
|
12
|
+
result = result.replace(/~~(.+?)~~/g, '$1');
|
|
13
|
+
result = result.replace(/`([^`]+)`/g, '$1');
|
|
14
|
+
result = result.replace(/^#{1,6}\s+/gm, '');
|
|
15
|
+
result = result.replace(/^>\s?/gm, '');
|
|
16
|
+
result = result.replace(/^[-*_]{3,}$/gm, '');
|
|
17
|
+
result = result.replace(/^(\s*)[-*+]\s/gm, '$1');
|
|
18
|
+
result = result.replace(/^(\s*)\d+\.\s/gm, '$1');
|
|
19
|
+
return result.trim();
|
|
20
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 统一媒体下载/缓存/SSRF防护框架
|
|
3
|
+
*
|
|
4
|
+
* 提供跨通道复用的文件处理能力:
|
|
5
|
+
* - 文件保存到 uploads 目录(自动去重、文件名清洗)
|
|
6
|
+
* - 图片类型白名单 + 大小限制
|
|
7
|
+
* - URL SSRF 防护(域名白名单 + 私有 IP 拦截)
|
|
8
|
+
* - 下载缓存(相同 key 不重复下载)
|
|
9
|
+
*/
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import crypto from 'crypto';
|
|
13
|
+
import { logger } from './logger.js';
|
|
14
|
+
// ── 常量 ──────────────────────────────────────────────────────────────────────
|
|
15
|
+
const DEFAULT_MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
16
|
+
const DEFAULT_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB
|
|
17
|
+
const ALLOWED_IMAGE_MIMES = new Set([
|
|
18
|
+
'image/png', 'image/jpeg', 'image/gif', 'image/webp',
|
|
19
|
+
]);
|
|
20
|
+
const UPLOADS_SUBDIR = '.evolcore/uploads';
|
|
21
|
+
// ── SSRF 防护 ─────────────────────────────────────────────────────────────────
|
|
22
|
+
/** 已知安全的 CDN 域名白名单 */
|
|
23
|
+
const ALLOWED_CDN_HOSTS = new Set([
|
|
24
|
+
'novac2c.cdn.weixin.qq.com',
|
|
25
|
+
'open.feishu.cn',
|
|
26
|
+
'internal-api-lark-file.feishu.cn',
|
|
27
|
+
'oapi.dingtalk.com',
|
|
28
|
+
'api.dingtalk.com',
|
|
29
|
+
]);
|
|
30
|
+
/** 私有 IP 段正则(IPv4) */
|
|
31
|
+
const PRIVATE_IP_RE = /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|127\.|0\.|169\.254\.|::1|fc|fd|fe80)/;
|
|
32
|
+
/**
|
|
33
|
+
* 检查 URL 是否安全(非 SSRF)
|
|
34
|
+
* - 拒绝私有 IP
|
|
35
|
+
* - 仅允许 https(或白名单域名的 http)
|
|
36
|
+
* - 可选:域名白名单模式
|
|
37
|
+
*/
|
|
38
|
+
export function validateUrl(url, opts) {
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = new URL(url);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { ok: false, reason: `Invalid URL: ${url}` };
|
|
45
|
+
}
|
|
46
|
+
// 协议检查
|
|
47
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
48
|
+
return { ok: false, reason: `Blocked protocol: ${parsed.protocol}` };
|
|
49
|
+
}
|
|
50
|
+
// 私有 IP 检查
|
|
51
|
+
const host = parsed.hostname;
|
|
52
|
+
if (PRIVATE_IP_RE.test(host)) {
|
|
53
|
+
return { ok: false, reason: `Blocked private IP: ${host}` };
|
|
54
|
+
}
|
|
55
|
+
// 域名白名单(如果提供)
|
|
56
|
+
const allowed = opts?.allowedHosts ?? ALLOWED_CDN_HOSTS;
|
|
57
|
+
if (allowed.size > 0 && !allowed.has(host)) {
|
|
58
|
+
return { ok: false, reason: `Host not in allowlist: ${host}` };
|
|
59
|
+
}
|
|
60
|
+
return { ok: true };
|
|
61
|
+
}
|
|
62
|
+
// ── 文件名清洗 ────────────────────────────────────────────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* 清洗文件名:
|
|
65
|
+
* - 移除路径穿越字符(只保留 basename)
|
|
66
|
+
* - 替换非法字符
|
|
67
|
+
* - 空结果兜底
|
|
68
|
+
*/
|
|
69
|
+
export function sanitizeFileName(name) {
|
|
70
|
+
return path.basename(name).replace(/[<>:"|?*\x00-\x1f]/g, '_') || `file_${Date.now()}`;
|
|
71
|
+
}
|
|
72
|
+
// ── 图片验证 ──────────────────────────────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* 验证图片 Buffer:类型白名单 + 大小限制
|
|
75
|
+
* 返回检测到的 MIME 类型,验证失败返回 null + reason
|
|
76
|
+
*/
|
|
77
|
+
export async function validateImage(buffer, opts) {
|
|
78
|
+
const maxSize = opts?.maxSize ?? DEFAULT_MAX_IMAGE_SIZE;
|
|
79
|
+
const allowed = opts?.allowedMimes ?? ALLOWED_IMAGE_MIMES;
|
|
80
|
+
if (buffer.length === 0) {
|
|
81
|
+
return { mime: null, reason: 'Empty buffer' };
|
|
82
|
+
}
|
|
83
|
+
if (buffer.length > maxSize) {
|
|
84
|
+
return { mime: null, reason: `Image too large: ${buffer.length} bytes (max ${maxSize})` };
|
|
85
|
+
}
|
|
86
|
+
// 动态导入 image-type(ESM only)
|
|
87
|
+
const { default: imageType } = await import('image-type');
|
|
88
|
+
// image-type 对极短/截断的 buffer 会抛 EndOfStreamError,捕获后按「无法识别」处理。
|
|
89
|
+
let type;
|
|
90
|
+
try {
|
|
91
|
+
type = await imageType(buffer);
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
return { mime: null, reason: `Image type detection error: ${e.message}` };
|
|
95
|
+
}
|
|
96
|
+
if (!type) {
|
|
97
|
+
return { mime: null, reason: 'Unable to detect image type' };
|
|
98
|
+
}
|
|
99
|
+
if (!allowed.has(type.mime)) {
|
|
100
|
+
return { mime: null, reason: `Unsupported image type: ${type.mime}` };
|
|
101
|
+
}
|
|
102
|
+
return { mime: type.mime };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 把已下载的附件 Buffer 转成入站图片条目(供 baseagent 视觉通道)。
|
|
106
|
+
*
|
|
107
|
+
* 适用于所有「需要先下载再注入」的通道(AUN ticket 下载、未来其它 CDN 下载等)。
|
|
108
|
+
* 飞书/微信已有各自的 SDK 下载路径,可按需复用本函数统一 MIME 判定。
|
|
109
|
+
*
|
|
110
|
+
* 判定优先级:
|
|
111
|
+
* 1. magic bytes(image-type 库,最可靠,同时做大小/白名单校验)
|
|
112
|
+
* 2. 元数据 MIME 字段(att.content_type / mime_type / mimeType)
|
|
113
|
+
* 3. 文件名后缀
|
|
114
|
+
*
|
|
115
|
+
* @returns 是图片返回 InboundImage;非图片或校验失败返回 null。
|
|
116
|
+
*/
|
|
117
|
+
export async function bufferToInboundImage(buffer, hints) {
|
|
118
|
+
// 1. magic bytes(含大小 + 白名单校验;validateImage 已吞掉 image-type 的异常)
|
|
119
|
+
const validated = await validateImage(buffer);
|
|
120
|
+
if (validated.mime) {
|
|
121
|
+
return { data: buffer.toString('base64'), mimeType: validated.mime };
|
|
122
|
+
}
|
|
123
|
+
// 2/3. magic bytes 未识别时,回退到元数据字段 / 文件名后缀
|
|
124
|
+
// (仍受 image 白名单约束,避免把任意文件当图片注入)
|
|
125
|
+
const metaCt = hints?.contentType || hints?.mimeType || '';
|
|
126
|
+
const byMeta = typeof metaCt === 'string' && ALLOWED_IMAGE_MIMES.has(metaCt) ? metaCt : '';
|
|
127
|
+
const byExt = hints?.filename
|
|
128
|
+
? (ALLOWED_IMAGE_MIMES.has(guessMime(hints.filename)) ? guessMime(hints.filename) : '')
|
|
129
|
+
: '';
|
|
130
|
+
const fallback = byMeta || byExt;
|
|
131
|
+
if (fallback && buffer.length > 0 && buffer.length <= DEFAULT_MAX_IMAGE_SIZE) {
|
|
132
|
+
return { data: buffer.toString('base64'), mimeType: fallback };
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 保存 Buffer 到 uploads 目录
|
|
138
|
+
* - 自动创建目录
|
|
139
|
+
* - 文件名清洗
|
|
140
|
+
* - 同名文件自动添加时间戳后缀
|
|
141
|
+
*/
|
|
142
|
+
export function saveToUploads(buffer, rawFileName, projectPath, opts) {
|
|
143
|
+
const fileName = sanitizeFileName(rawFileName);
|
|
144
|
+
const uploadsDir = path.join(projectPath, UPLOADS_SUBDIR);
|
|
145
|
+
fs.mkdirSync(uploadsDir, { recursive: true });
|
|
146
|
+
let targetName = fileName;
|
|
147
|
+
const targetPath = path.join(uploadsDir, targetName);
|
|
148
|
+
// 去重:内容相同则复用
|
|
149
|
+
if (opts?.dedup !== false && fs.existsSync(targetPath)) {
|
|
150
|
+
const existingHash = hashFile(targetPath);
|
|
151
|
+
const newHash = crypto.createHash('md5').update(buffer).digest('hex');
|
|
152
|
+
if (existingHash === newHash) {
|
|
153
|
+
logger.debug(`[MediaCache] Dedup hit: ${targetName}`);
|
|
154
|
+
return { filePath: targetPath, fileName: targetName, size: buffer.length };
|
|
155
|
+
}
|
|
156
|
+
// 不同内容同名 → 加时间戳
|
|
157
|
+
const ext = path.extname(fileName);
|
|
158
|
+
const base = path.basename(fileName, ext);
|
|
159
|
+
targetName = `${base}_${Date.now()}${ext}`;
|
|
160
|
+
}
|
|
161
|
+
const finalPath = path.join(uploadsDir, targetName);
|
|
162
|
+
fs.writeFileSync(finalPath, buffer);
|
|
163
|
+
logger.info(`[MediaCache] Saved: ${finalPath} (${buffer.length} bytes)`);
|
|
164
|
+
return { filePath: finalPath, fileName: targetName, size: buffer.length };
|
|
165
|
+
}
|
|
166
|
+
function hashFile(filePath) {
|
|
167
|
+
const content = fs.readFileSync(filePath);
|
|
168
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 安全下载 URL 内容,带 SSRF 防护
|
|
172
|
+
*/
|
|
173
|
+
export async function safeFetch(url, opts) {
|
|
174
|
+
const maxSize = opts?.maxSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
175
|
+
const timeout = opts?.timeout ?? 30_000;
|
|
176
|
+
// SSRF 检查
|
|
177
|
+
if (!opts?.skipSsrfCheck) {
|
|
178
|
+
const check = validateUrl(url, { allowedHosts: opts?.allowedHosts });
|
|
179
|
+
if (!check.ok) {
|
|
180
|
+
throw new Error(`SSRF blocked: ${check.reason}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const controller = new AbortController();
|
|
184
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
185
|
+
try {
|
|
186
|
+
const res = await fetch(url, {
|
|
187
|
+
signal: controller.signal,
|
|
188
|
+
redirect: opts?.rejectRedirects ? 'error' : 'follow',
|
|
189
|
+
});
|
|
190
|
+
if (!res.ok)
|
|
191
|
+
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
192
|
+
// 检查 Content-Length(如果可用)
|
|
193
|
+
const contentLength = res.headers.get('content-length');
|
|
194
|
+
if (contentLength && parseInt(contentLength) > maxSize) {
|
|
195
|
+
throw new Error(`File too large: ${contentLength} bytes (max ${maxSize})`);
|
|
196
|
+
}
|
|
197
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
198
|
+
if (buffer.length > maxSize) {
|
|
199
|
+
throw new Error(`File too large: ${buffer.length} bytes (max ${maxSize})`);
|
|
200
|
+
}
|
|
201
|
+
return buffer;
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* 内存级下载缓存
|
|
209
|
+
* - 相同 key 不重复下载
|
|
210
|
+
* - TTL 过期自动清理
|
|
211
|
+
* - 最大条目限制防止内存泄漏
|
|
212
|
+
*/
|
|
213
|
+
export class DownloadCache {
|
|
214
|
+
cache = new Map();
|
|
215
|
+
ttlMs;
|
|
216
|
+
maxEntries;
|
|
217
|
+
constructor(opts) {
|
|
218
|
+
this.ttlMs = opts?.ttlMs ?? 5 * 60 * 1000; // 5 min
|
|
219
|
+
this.maxEntries = opts?.maxEntries ?? 100;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 获取或下载:缓存命中直接返回,否则执行 fetcher 并缓存结果
|
|
223
|
+
*/
|
|
224
|
+
async getOrFetch(key, fetcher) {
|
|
225
|
+
this.evict();
|
|
226
|
+
const cached = this.cache.get(key);
|
|
227
|
+
if (cached) {
|
|
228
|
+
logger.debug(`[DownloadCache] Hit: ${key}`);
|
|
229
|
+
return cached.buffer;
|
|
230
|
+
}
|
|
231
|
+
const buffer = await fetcher();
|
|
232
|
+
this.cache.set(key, { buffer, createdAt: Date.now() });
|
|
233
|
+
// 超过 maxEntries 时删最旧
|
|
234
|
+
if (this.cache.size > this.maxEntries) {
|
|
235
|
+
const oldest = this.cache.keys().next().value;
|
|
236
|
+
this.cache.delete(oldest);
|
|
237
|
+
}
|
|
238
|
+
return buffer;
|
|
239
|
+
}
|
|
240
|
+
evict() {
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
for (const [key, entry] of this.cache) {
|
|
243
|
+
if (now - entry.createdAt > this.ttlMs) {
|
|
244
|
+
this.cache.delete(key);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
clear() {
|
|
249
|
+
this.cache.clear();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// ── MIME / Size helpers ──────────────────────────────────────────────────────
|
|
253
|
+
const MIME_MAP = {
|
|
254
|
+
'.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json',
|
|
255
|
+
'.js': 'text/javascript', '.ts': 'text/typescript', '.py': 'text/x-python',
|
|
256
|
+
'.html': 'text/html', '.css': 'text/css', '.csv': 'text/csv',
|
|
257
|
+
'.pdf': 'application/pdf', '.zip': 'application/zip', '.gz': 'application/gzip',
|
|
258
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
259
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
260
|
+
'.xml': 'application/xml', '.yaml': 'application/x-yaml', '.yml': 'application/x-yaml',
|
|
261
|
+
'.mp4': 'video/mp4', '.mov': 'video/quicktime', '.webm': 'video/webm', '.avi': 'video/x-msvideo',
|
|
262
|
+
'.opus': 'audio/ogg', '.mp3': 'audio/mpeg', '.aac': 'audio/aac', '.m4a': 'audio/mp4', '.wav': 'audio/wav',
|
|
263
|
+
};
|
|
264
|
+
export function guessMime(filename) {
|
|
265
|
+
const ext = path.extname(filename).toLowerCase();
|
|
266
|
+
return MIME_MAP[ext] || 'application/octet-stream';
|
|
267
|
+
}
|
|
268
|
+
export function formatSize(bytes) {
|
|
269
|
+
if (bytes < 1024)
|
|
270
|
+
return `${bytes} B`;
|
|
271
|
+
if (bytes < 1048576)
|
|
272
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
273
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
274
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{"model":"claude-opus-4-8","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":15,"price_output":75,"price_cache_creation":18.75,"price_cache_read":1.5}
|
|
2
|
+
{"model":"claude-opus-4-7","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":15,"price_output":75,"price_cache_creation":18.75,"price_cache_read":1.5}
|
|
3
|
+
{"model":"claude-opus-4-6","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":15,"price_output":75,"price_cache_creation":18.75,"price_cache_read":1.5}
|
|
4
|
+
{"model":"claude-sonnet-4-6","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":3,"price_output":15,"price_cache_creation":3.75,"price_cache_read":0.3}
|
|
5
|
+
{"model":"claude-haiku-4-5-20251001","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":0.8,"price_output":4,"price_cache_creation":1,"price_cache_read":0.08}
|
|
6
|
+
{"model":"gpt-5.6-sol","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":5,"price_output":30,"price_cache_creation":6.25,"price_cache_read":0.5,"tiers":[{"up_to_input_tokens":272000,"price_input":5,"price_output":30,"price_cache_creation":6.25,"price_cache_read":0.5},{"up_to_input_tokens":null,"price_input":10,"price_output":45,"price_cache_creation":12.5,"price_cache_read":1}]}
|
|
7
|
+
{"model":"gpt-5.6-terra","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":2.5,"price_output":15,"price_cache_creation":3.125,"price_cache_read":0.25,"tiers":[{"up_to_input_tokens":272000,"price_input":2.5,"price_output":15,"price_cache_creation":3.125,"price_cache_read":0.25},{"up_to_input_tokens":null,"price_input":5,"price_output":22.5,"price_cache_creation":6.25,"price_cache_read":0.5}]}
|
|
8
|
+
{"model":"gpt-5.6-luna","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":1,"price_output":6,"price_cache_creation":1.25,"price_cache_read":0.1,"tiers":[{"up_to_input_tokens":272000,"price_input":1,"price_output":6,"price_cache_creation":1.25,"price_cache_read":0.1},{"up_to_input_tokens":null,"price_input":2,"price_output":9,"price_cache_creation":2.5,"price_cache_read":0.2}]}
|
|
9
|
+
{"model":"gpt-5.5","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":5,"price_output":30,"price_cache_creation":0,"price_cache_read":0.5,"tiers":[{"up_to_input_tokens":272000,"price_input":5,"price_output":30,"price_cache_creation":0,"price_cache_read":0.5},{"up_to_input_tokens":null,"price_input":10,"price_output":45,"price_cache_creation":0,"price_cache_read":1}]}
|
|
10
|
+
{"model":"gpt-5.4","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":2.5,"price_output":15,"price_cache_creation":0,"price_cache_read":0.25,"tiers":[{"up_to_input_tokens":272000,"price_input":2.5,"price_output":15,"price_cache_creation":0,"price_cache_read":0.25},{"up_to_input_tokens":null,"price_input":5,"price_output":22.5,"price_cache_creation":0,"price_cache_read":0.5}]}
|
|
11
|
+
{"model":"gpt-5.3-codex","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":1.75,"price_output":14,"price_cache_creation":0,"price_cache_read":0.175}
|
|
12
|
+
{"model":"deepseek-v4-pro","effective_from":0,"billing_fn":"per_token_deepseek_v1","currency":"CNY","price_cache_hit":0.5,"price_cache_miss":2,"price_output":8}
|
|
13
|
+
{"model":"deepseek-v4-flash","effective_from":0,"billing_fn":"per_token_deepseek_v1","currency":"CNY","price_cache_hit":0.1,"price_cache_miss":0.5,"price_output":2}
|
|
14
|
+
{"model":"deepseek-v3.2","effective_from":0,"billing_fn":"per_token_deepseek_v1","currency":"CNY","price_cache_hit":0.5,"price_cache_miss":2,"price_output":8}
|
|
15
|
+
{"model":"kimi-k2.6","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":2,"price_output":8,"price_cache_creation":0,"price_cache_read":0}
|
|
16
|
+
{"model":"kimi-k2.5","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":2,"price_output":8,"price_cache_creation":0,"price_cache_read":0}
|
|
17
|
+
{"model":"glm-5.1","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":5,"price_output":20,"price_cache_creation":0,"price_cache_read":0}
|
|
18
|
+
{"model":"glm-5","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":5,"price_output":20,"price_cache_creation":0,"price_cache_read":0}
|
|
19
|
+
{"model":"glm-4.7","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":1,"price_output":5,"price_cache_creation":0,"price_cache_read":0}
|
|
20
|
+
{"model":"MiniMax-M2.7","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":1,"price_output":5,"price_cache_creation":0,"price_cache_read":0}
|