@xopcai/xopc 0.0.91 → 0.0.93
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/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/feishu/src/outbound/media-load.js +1 -1
- package/dist/extensions/feishu/src/workflow-progress.js +1 -1
- package/dist/extensions/telegram/src/plugin.js +1 -1
- package/dist/extensions/telegram/src/routing-integration.js +2 -2
- package/dist/extensions/telegram/src/workflow-progress.js +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/extensions/weixin/src/api/api.js +2 -2
- package/dist/extensions/weixin/src/auth/accounts.js +1 -1
- package/dist/extensions/weixin/src/cdn/upload.js +1 -1
- package/dist/extensions/weixin/src/media/data-url.js +1 -1
- package/dist/extensions/weixin/src/messaging/debug-mode.js +1 -1
- package/dist/extensions/weixin/src/messaging/inbound.js +1 -1
- package/dist/extensions/weixin/src/messaging/process-message.js +1 -1
- package/dist/extensions/weixin/src/plugin.js +1 -1
- package/dist/extensions/weixin/src/storage/sync-buf.js +1 -1
- package/dist/extensions/weixin/src/workflow-progress.js +1 -1
- package/dist/gateway/static/root/assets/agents-C7tTJLP9.js +222 -0
- package/dist/gateway/static/root/assets/apps-page-BbzdMyrg.js +1 -0
- package/dist/gateway/static/root/assets/channels-settings-B49vG2hE.js +1 -0
- package/dist/gateway/static/root/assets/{channels-status-swr-CYWL5DLD.js → channels-status-swr-CsGkK9h9.js} +1 -1
- package/dist/gateway/static/root/assets/{cron-api-TVqLlGAC.js → cron-api-CyAm0xJT.js} +1 -1
- package/dist/gateway/static/root/assets/cron-page-Bjx7IOdR.js +1 -0
- package/dist/gateway/static/root/assets/{dist-CUV1uY5f.js → dist-DHwVV8XK.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-mTLHRDp1.js → extension-debug-page-BK8kcc4F.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-iI8BI7WK.js → extension-page-Cf8X_QUc.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-ByXcdubM.js → extension-settings-page-C5-YLMmy.js} +1 -1
- package/dist/gateway/static/root/assets/{fetch-BWtQq_Ys.js → fetch-BAPnkYbC.js} +1 -1
- package/dist/gateway/static/root/assets/{field-primitives-BsZ-4VT5.js → field-primitives-8p7ucXa1.js} +1 -1
- package/dist/gateway/static/root/assets/heartbeat-config-api-CpgW2sGp.js +1 -0
- package/dist/gateway/static/root/assets/index-CwDdudZM.css +1 -0
- package/dist/gateway/static/root/assets/{index-CKkR-v9U.js → index-Do52EfZK.js} +82 -82
- package/dist/gateway/static/root/assets/logs-page-BxukQ-J-.js +1 -0
- package/dist/gateway/static/root/assets/{note-detail-page-DJ2Mb4x7.js → note-detail-page-WLM6FUIi.js} +53 -53
- package/dist/gateway/static/root/assets/{note-time-JLBPSLzK.js → note-time-EFyIVhec.js} +1 -1
- package/dist/gateway/static/root/assets/notes-page-BYPVYcYn.js +1 -0
- package/dist/gateway/static/root/assets/sessions-page-BFD2_-Cl.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-CEs8pGh6.js +2 -0
- package/dist/gateway/static/root/assets/{settings-form-section-DSYCknxM.js → settings-form-section-C6cGTVwK.js} +1 -1
- package/dist/gateway/static/root/assets/settings-page-BiP5iH46.js +2 -0
- package/dist/gateway/static/root/assets/{share-preview-page-awRqs4hV.js → share-preview-page-tnIfJ4K6.js} +1 -1
- package/dist/gateway/static/root/assets/skills-page-CNDctFIn.js +2 -0
- package/dist/gateway/static/root/assets/{theme-store-BC-42BoZ.js → theme-store-D6EsNTPr.js} +1 -1
- package/dist/gateway/static/root/assets/url-CTjpm0Uz.js +3 -0
- package/dist/gateway/static/root/assets/{utils-DX3TQuap.js → utils-C86AVfY-.js} +1 -1
- package/dist/gateway/static/root/assets/voice-api-key-field-CalxUkxm.js +1 -0
- package/dist/gateway/static/root/assets/{workflow-page.utils-ClC37yEp.js → workflow-page.utils-DsEriMFW.js} +1 -1
- package/dist/gateway/static/root/assets/workflows-page-D2fRxXJG.js +27 -0
- package/dist/gateway/static/root/index.html +5 -5
- package/dist/package.js +1 -1
- package/dist/src/agent/agent-manager.js +7 -7
- package/dist/src/agent/agent-scope.js +1 -1
- package/dist/src/agent/bootstrap/load-bootstrap-files.js +1 -1
- package/dist/src/agent/context/workspace-seed.js +2 -2
- package/dist/src/agent/goals/goal-run-store.js +4 -4
- package/dist/src/agent/goals/persistent-goal-service.js +1 -1
- package/dist/src/agent/goals/post-turn.js +2 -2
- package/dist/src/agent/image/load-image-media.js +2 -2
- package/dist/src/agent/ipc/bus.js +1 -1
- package/dist/src/agent/ipc/inbox.js +2 -2
- package/dist/src/agent/ipc/socket.js +1 -1
- package/dist/src/agent/mcp/bundle-mcp-config.d.ts +2 -9
- package/dist/src/agent/mcp/bundle-mcp-config.js +10 -34
- package/dist/src/agent/mcp/bundle-mcp-config.js.map +1 -1
- package/dist/src/agent/mcp/bundle-mcp-materialize.js +1 -1
- package/dist/src/agent/mcp/bundle-mcp-policy.js +2 -2
- package/dist/src/agent/mcp/bundle-mcp-policy.js.map +1 -1
- package/dist/src/agent/mcp/bundle-mcp-runtime.js +5 -5
- package/dist/src/agent/mcp/bundle-mcp-runtime.js.map +1 -1
- package/dist/src/agent/mcp/index.js +2 -2
- package/dist/src/agent/mcp/mcp-transport-config.js +1 -1
- package/dist/src/agent/mcp/mcp-transport.js +1 -1
- package/dist/src/agent/memory/builtin-memory-store.js +1 -1
- package/dist/src/agent/memory/dreaming/deep-promotion.js +1 -1
- package/dist/src/agent/memory/dreaming/events.js +1 -1
- package/dist/src/agent/memory/dreaming/last-run.js +1 -1
- package/dist/src/agent/memory/dreaming/light-sweep.js +1 -1
- package/dist/src/agent/memory/dreaming/preview.js +1 -1
- package/dist/src/agent/memory/dreaming/rem-patterns.js +1 -1
- package/dist/src/agent/memory/dreaming/short-term-store.js +1 -1
- package/dist/src/agent/memory/dreaming/utils.js +1 -1
- package/dist/src/agent/memory/plugin-discovery.js +1 -1
- package/dist/src/agent/models/manager.js +1 -1
- package/dist/src/agent/prompt/service-prompt-builder.js +2 -2
- package/dist/src/agent/reply/post-compaction-context.js +1 -1
- package/dist/src/agent/reply/workspace-boundary-read.js +1 -1
- package/dist/src/agent/sandbox/path-policy.js +2 -2
- package/dist/src/agent/service/build-direct-message-content.js +1 -1
- package/dist/src/agent/service.js +4 -4
- package/dist/src/agent/session/session-inspector.js +1 -1
- package/dist/src/agent/skills/config.js +1 -1
- package/dist/src/agent/skills/hub-hash.js +2 -2
- package/dist/src/agent/skills/hub-lock.js +1 -1
- package/dist/src/agent/skills/hub-pull.js +2 -2
- package/dist/src/agent/skills/index.js +1 -1
- package/dist/src/agent/skills/managed-store.js +1 -1
- package/dist/src/agent/skills/scanner.js +1 -1
- package/dist/src/agent/skills/skill-manage-ops.js +1 -1
- package/dist/src/agent/skills/skill-manager.js +1 -1
- package/dist/src/agent/tools/dreaming-tool.js +1 -1
- package/dist/src/agent/tools/factory.js +1 -1
- package/dist/src/agent/tools/image-generate-tool.js +1 -1
- package/dist/src/agent/tools/send-media.js +1 -1
- package/dist/src/agent/tools/skill-manage-tool.js +1 -1
- package/dist/src/agent/tools/workflow-tool.js +1 -1
- package/dist/src/agent/tools/write.js +1 -1
- package/dist/src/agent/workflow/catalog.js +1 -1
- package/dist/src/auth/credentials.js +3 -3
- package/dist/src/auth/profiles/store.js +1 -1
- package/dist/src/auth/sync-provider-auth.js +1 -1
- package/dist/src/browser/cache-dir-policy.js +1 -1
- package/dist/src/browser/cdp-local-launcher.js +2 -2
- package/dist/src/browser/providers/browser-ext-install.js +3 -3
- package/dist/src/browser/providers/cloakbrowser.js +4 -4
- package/dist/src/browser/providers/playwright-doctor.js +1 -1
- package/dist/src/browser/stealth.js +1 -1
- package/dist/src/channels/attachments/inbound-persist.js +1 -1
- package/dist/src/channels/attachments/outbound-tts-persist.js +1 -1
- package/dist/src/channels/outbound/persist-store.js +1 -1
- package/dist/src/channels/pairing/allow-from-file.js +1 -1
- package/dist/src/channels/pairing/pairing-store.js +2 -2
- package/dist/src/chat-commands/agent-edit.js +2 -2
- package/dist/src/chat-commands/builtins/config.js +2 -2
- package/dist/src/chat-commands/context.js +1 -1
- package/dist/src/cli/command-catalog.js +0 -4
- package/dist/src/cli/command-catalog.js.map +1 -1
- package/dist/src/cli/command-loaders.js +1 -2
- package/dist/src/cli/command-loaders.js.map +1 -1
- package/dist/src/cli/command-manifest.js +0 -4
- package/dist/src/cli/command-manifest.js.map +1 -1
- package/dist/src/cli/commands/config.js +1 -1
- package/dist/src/cli/commands/doctor/checks/config-health.js +1 -1
- package/dist/src/cli/commands/doctor/checks/provider-auth.js +1 -1
- package/dist/src/cli/commands/doctor/checks/session-integrity.js +2 -2
- package/dist/src/cli/commands/doctor/checks/state-integrity.js +1 -1
- package/dist/src/cli/commands/doctor/checks/workspace-status.js +1 -1
- package/dist/src/cli/commands/extension-dev.js +1 -1
- package/dist/src/cli/commands/extension-marketplace.js +1 -1
- package/dist/src/cli/commands/extension-pack.js +1 -1
- package/dist/src/cli/commands/gateway/logs.js +1 -1
- package/dist/src/cli/commands/image.js +1 -1
- package/dist/src/cli/commands/init.js +4 -4
- package/dist/src/cli/commands/onboard.js +1 -1
- package/dist/src/cli/utils/init-workspace-core.js +2 -2
- package/dist/src/commands/agents.config.js +1 -1
- package/dist/src/config/agent-profile.js +1 -1
- package/dist/src/config/gateway-bind.js +1 -1
- package/dist/src/config/index.d.ts +0 -1
- package/dist/src/config/index.js +6 -7
- package/dist/src/config/index.js.map +1 -1
- package/dist/src/config/loader.js +2 -2
- package/dist/src/config/models-json.js +2 -2
- package/dist/src/config/paths-state.js +1 -1
- package/dist/src/config/profile.js +2 -2
- package/dist/src/config/schema.d.ts +36 -6
- package/dist/src/config/schema.js +13 -11
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/config/workspace-path.js +1 -1
- package/dist/src/connectors/builtin-catalog.d.ts +2 -0
- package/dist/src/connectors/builtin-catalog.js +152 -0
- package/dist/src/connectors/builtin-catalog.js.map +1 -0
- package/dist/src/connectors/catalog.d.ts +5 -0
- package/dist/src/connectors/catalog.js +13 -0
- package/dist/src/connectors/catalog.js.map +1 -0
- package/dist/src/connectors/health.d.ts +3 -0
- package/dist/src/connectors/health.js +61 -0
- package/dist/src/connectors/health.js.map +1 -0
- package/dist/src/connectors/install.d.ts +5 -0
- package/dist/src/connectors/install.js +46 -0
- package/dist/src/connectors/install.js.map +1 -0
- package/dist/src/connectors/instances.d.ts +4 -0
- package/dist/src/connectors/instances.js +43 -0
- package/dist/src/connectors/instances.js.map +1 -0
- package/dist/src/connectors/materialize.d.ts +9 -0
- package/dist/src/connectors/materialize.js +76 -0
- package/dist/src/connectors/materialize.js.map +1 -0
- package/dist/src/connectors/oauth.d.ts +22 -0
- package/dist/src/connectors/oauth.js +99 -0
- package/dist/src/connectors/oauth.js.map +1 -0
- package/dist/src/connectors/providers.d.ts +9 -0
- package/dist/src/connectors/providers.js +20 -0
- package/dist/src/connectors/providers.js.map +1 -0
- package/dist/src/connectors/secret-store.d.ts +7 -0
- package/dist/src/connectors/secret-store.js +47 -0
- package/dist/src/connectors/secret-store.js.map +1 -0
- package/dist/src/connectors/types.d.ts +102 -0
- package/dist/src/connectors/types.js +1 -0
- package/dist/src/connectors/usage.d.ts +6 -0
- package/dist/src/connectors/usage.js +63 -0
- package/dist/src/connectors/usage.js.map +1 -0
- package/dist/src/cron/executor.js +2 -2
- package/dist/src/cron/persistence.js +1 -1
- package/dist/src/cron/run-log-store.js +1 -1
- package/dist/src/daemon/constants.js +1 -1
- package/dist/src/daemon/install-plan.js +2 -2
- package/dist/src/daemon/launchd.js +2 -2
- package/dist/src/daemon/schtasks.js +2 -2
- package/dist/src/daemon/systemd.js +2 -2
- package/dist/src/extensions/bundle-mcp.js +1 -1
- package/dist/src/extensions/discover-extensions.js +1 -1
- package/dist/src/extensions/health.js +1 -1
- package/dist/src/extensions/loader.js +1 -1
- package/dist/src/extensions/lockfile.js +2 -2
- package/dist/src/extensions/update.js +1 -1
- package/dist/src/gateway/agents-admin.js +3 -3
- package/dist/src/gateway/file-path-classifier.js +2 -2
- package/dist/src/gateway/hono/lib/config-payload.js +1 -1
- package/dist/src/gateway/hono/lib/extension-store.js +2 -2
- package/dist/src/gateway/hono/lib/static-ui.js +2 -2
- package/dist/src/gateway/hono/oauth.js +1 -1
- package/dist/src/gateway/hono/routes/agents.js +1 -1
- package/dist/src/gateway/hono/routes/auth-registry-extensions.js +1 -1
- package/dist/src/gateway/hono/routes/config-patch/misc.js +1 -1
- package/dist/src/gateway/hono/routes/connectors.d.ts +3 -0
- package/dist/src/gateway/hono/routes/connectors.js +177 -0
- package/dist/src/gateway/hono/routes/connectors.js.map +1 -0
- package/dist/src/gateway/hono/routes/dreaming.js +1 -1
- package/dist/src/gateway/hono/routes/home.d.ts +12 -0
- package/dist/src/gateway/hono/routes/home.js +50 -0
- package/dist/src/gateway/hono/routes/home.js.map +1 -0
- package/dist/src/gateway/hono/routes/host-fs.js +2 -2
- package/dist/src/gateway/hono/routes/lazy-bundles.js +12 -4
- package/dist/src/gateway/hono/routes/lazy-bundles.js.map +1 -1
- package/dist/src/gateway/hono/routes/models.js +1 -1
- package/dist/src/gateway/hono/routes/notes.js +136 -1
- package/dist/src/gateway/hono/routes/notes.js.map +1 -1
- package/dist/src/gateway/hono/routes/shares.js +1 -1
- package/dist/src/gateway/hono/routes/workspace.js +4 -4
- package/dist/src/gateway/host.d.ts +2 -0
- package/dist/src/gateway/host.js +6 -3
- package/dist/src/gateway/host.js.map +1 -1
- package/dist/src/gateway/lock.js +3 -3
- package/dist/src/gateway/ports.js +1 -1
- package/dist/src/gateway/service/agent-runner.js +2 -2
- package/dist/src/gateway/service/marketplace-service.js +2 -2
- package/dist/src/gateway/service.js +1 -1
- package/dist/src/gateway/workspace-fs-file-list.js +1 -1
- package/dist/src/infra/brew.js +1 -1
- package/dist/src/infra/package-json.js +1 -1
- package/dist/src/infra/package-update-steps.js +1 -1
- package/dist/src/infra/path-env.js +2 -2
- package/dist/src/infra/restart.js +2 -2
- package/dist/src/infra/stable-node-path.js +1 -1
- package/dist/src/infra/update-check.js +1 -1
- package/dist/src/infra/update-global.js +1 -1
- package/dist/src/infra/update-lock.js +3 -3
- package/dist/src/infra/update-runner.js +1 -1
- package/dist/src/infra/update-startup.js +2 -2
- package/dist/src/infra/write-file-atomic.js +2 -2
- package/dist/src/mcp/channel-bridge.js +1 -1
- package/dist/src/mcp/channel-bridge.js.map +1 -1
- package/dist/src/notes/index.d.ts +1 -1
- package/dist/src/notes/service.d.ts +24 -1
- package/dist/src/notes/service.js +279 -0
- package/dist/src/notes/service.js.map +1 -1
- package/dist/src/notes/store.d.ts +4 -0
- package/dist/src/notes/store.js +37 -8
- package/dist/src/notes/store.js.map +1 -1
- package/dist/src/notes/types.d.ts +70 -2
- package/dist/src/providers/auth-runtime/auth-profile-store.js +1 -1
- package/dist/src/providers/index.js +2 -2
- package/dist/src/providers/model-registry.js +1 -1
- package/dist/src/session/config-store.js +2 -2
- package/dist/src/session/init-session-turn.js +2 -2
- package/dist/src/session/parity/jsonl-transcript-io.js +2 -2
- package/dist/src/session/parity/sessions-json-file.js +1 -1
- package/dist/src/session/parity/transcript-file-lock.js +2 -2
- package/dist/src/session/parity/transcript-paths.js +1 -1
- package/dist/src/session/resolve-session.js +4 -4
- package/dist/src/session/search-index-cache.js +1 -1
- package/dist/src/session/search-index.js +1 -1
- package/dist/src/session/session-title.js +2 -2
- package/dist/src/session/store.d.ts +2 -0
- package/dist/src/session/store.js +27 -7
- package/dist/src/session/store.js.map +1 -1
- package/dist/src/share/share-auto.js +2 -2
- package/dist/src/share/share-store.js +3 -3
- package/dist/src/share/share-thumbnail.js +2 -2
- package/dist/src/share/share-zip.js +1 -1
- package/dist/src/share/site-share-store.js +3 -3
- package/dist/src/share/site-static-serve.js +1 -1
- package/dist/src/tui/clipboard-image.js +3 -3
- package/dist/src/tui/theme-manager.js +1 -1
- package/dist/src/tui/tui-keybindings-file.js +1 -1
- package/dist/src/tui/tui-scoped-models.js +2 -2
- package/dist/src/tui/tui-settings.js +1 -1
- package/dist/src/tui/tui.js +3 -3
- package/dist/src/tunnel/frpc-binary.js +3 -3
- package/dist/src/tunnel/frpc-config.js +1 -1
- package/dist/src/tunnel/frpc-extract.js +1 -1
- package/dist/src/tunnel/tunnel-state.js +1 -1
- package/dist/src/utils/logger/audit.js +1 -1
- package/dist/src/utils/logger/log-store.js +1 -1
- package/dist/src/utils/logger/rotation.js +1 -1
- package/dist/src/voice/tts/audio.js +1 -1
- package/dist/src/voice/tts/providers/edge-speech.js +2 -2
- package/dist/src/workflows/store/event-store.js +1 -1
- package/dist/src/workflows/store/run-store.js +1 -1
- package/package.json +5 -1
- package/dist/gateway/static/root/assets/agents-bVWUlrlD.js +0 -222
- package/dist/gateway/static/root/assets/apps-page-CIC8bmvZ.js +0 -1
- package/dist/gateway/static/root/assets/channels-settings-C8G8RAAP.js +0 -1
- package/dist/gateway/static/root/assets/cron-dreaming-jobs-Ip703-qM.js +0 -2
- package/dist/gateway/static/root/assets/cron-page-BtcFYlvv.js +0 -1
- package/dist/gateway/static/root/assets/heartbeat-config-api-WjTsRLCU.js +0 -1
- package/dist/gateway/static/root/assets/index-VlELBY99.css +0 -1
- package/dist/gateway/static/root/assets/logs-page-ClnIpxfd.js +0 -1
- package/dist/gateway/static/root/assets/notes-page-BE-75qz9.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-bJJkWtTl.js +0 -1
- package/dist/gateway/static/root/assets/settings-page-WcMXLq2U.js +0 -3
- package/dist/gateway/static/root/assets/skills-page-Lu-i1JG7.js +0 -2
- package/dist/gateway/static/root/assets/url-CY1RQKTU.js +0 -3
- package/dist/gateway/static/root/assets/voice-api-key-field-B5uKlDqA.js +0 -1
- package/dist/gateway/static/root/assets/workflows-page-C7VhIXtR.js +0 -27
- package/dist/src/cli/commands/mcp.d.ts +0 -4
- package/dist/src/cli/commands/mcp.js +0 -85
- package/dist/src/cli/commands/mcp.js.map +0 -1
- package/dist/src/config/mcp-config.d.ts +0 -34
- package/dist/src/config/mcp-config.js +0 -116
- package/dist/src/config/mcp-config.js.map +0 -1
- package/dist/src/gateway/hono/routes/mcp.d.ts +0 -3
- package/dist/src/gateway/hono/routes/mcp.js +0 -99
- package/dist/src/gateway/hono/routes/mcp.js.map +0 -1
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { createLogger } from "../utils/logger/index.js";
|
|
2
2
|
import { init_logger } from "../utils/logger.js";
|
|
3
|
+
import { getDefaultModelSync, init_providers, resolveModel } from "../providers/index.js";
|
|
3
4
|
import { attachmentIdFromTarget, buildNoteAttachmentRef } from "./attachment-ref.js";
|
|
4
5
|
import { partitionAttachmentsByReference } from "./note-attachment-sync.js";
|
|
5
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { complete } from "@earendil-works/pi-ai";
|
|
6
8
|
//#region src/notes/service.ts
|
|
9
|
+
init_providers();
|
|
7
10
|
init_logger();
|
|
8
11
|
const log = createLogger("NotesService");
|
|
9
12
|
function inferKind(text, hasAttachments, attachments) {
|
|
@@ -15,6 +18,11 @@ function inferKind(text, hasAttachments, attachments) {
|
|
|
15
18
|
if (/^https?:\/\//.test(text.trim())) return "bookmark";
|
|
16
19
|
return "thought";
|
|
17
20
|
}
|
|
21
|
+
function deriveDefaultTitle(text) {
|
|
22
|
+
const normalizedText = text?.trim().replace(/\s+/g, " ");
|
|
23
|
+
if (!normalizedText) return void 0;
|
|
24
|
+
return Array.from(normalizedText).slice(0, 10).join("");
|
|
25
|
+
}
|
|
18
26
|
function createBlockId() {
|
|
19
27
|
return `block_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
20
28
|
}
|
|
@@ -103,6 +111,148 @@ function createAiOrganizedBlocks(blocks, instruction) {
|
|
|
103
111
|
updatedAt: now
|
|
104
112
|
}))];
|
|
105
113
|
}
|
|
114
|
+
function splitMeaningfulLines(text) {
|
|
115
|
+
return (text ?? "").split(/[\n。!?!?;;]+/).map((line) => line.trim().replace(/^[-*\d.\s]+/, "")).filter((line) => line.length > 0);
|
|
116
|
+
}
|
|
117
|
+
function summarizeIdea(text) {
|
|
118
|
+
const joined = splitMeaningfulLines(text).join(" ");
|
|
119
|
+
return Array.from(joined || "这个想法").slice(0, 90).join("");
|
|
120
|
+
}
|
|
121
|
+
function inferCatalysisStage(note) {
|
|
122
|
+
const text = `${note.title ?? ""}\n${note.text ?? ""}`;
|
|
123
|
+
if (/发布|上线|分享|推广|launch|ship/i.test(text)) return "shipped";
|
|
124
|
+
if (/验证|实验|用户|指标|反馈|validate|experiment/i.test(text)) return "validating";
|
|
125
|
+
if (/实现|开发|MVP|原型|prototype|build/i.test(text)) return "developing";
|
|
126
|
+
if (text.trim().length > 80) return "incubating";
|
|
127
|
+
return "seed";
|
|
128
|
+
}
|
|
129
|
+
function buildCatalysisReport(note) {
|
|
130
|
+
const generatedAt = Date.now();
|
|
131
|
+
const summary = summarizeIdea(note.text ?? note.title);
|
|
132
|
+
const title = note.title?.trim() || summary.slice(0, 28) || "未命名想法";
|
|
133
|
+
const hasUserSignal = /用户|客户|读者|创作者|团队|个人|开发者|founder|creator|user/i.test(summary);
|
|
134
|
+
const hasProductSignal = /产品|工具|平台|workflow|agent|AI|自动|系统|应用/i.test(summary);
|
|
135
|
+
const confidence = Math.min(.86, Math.max(.42, .48 + (hasUserSignal ? .16 : 0) + (hasProductSignal ? .18 : 0) + Math.min(summary.length, 120) / 600));
|
|
136
|
+
return {
|
|
137
|
+
originalNoteId: note.id,
|
|
138
|
+
generatedAt,
|
|
139
|
+
title,
|
|
140
|
+
valueHypothesis: `如果把「${summary}」推进成一个可体验的小成果,它最可能的价值是帮助用户更快完成判断、表达或行动。`,
|
|
141
|
+
targetUsers: hasUserSignal ? ["笔记作者自己", "有类似场景的目标用户"] : ["笔记作者自己", "未来可能被这个想法帮助的人"],
|
|
142
|
+
keyQuestions: [
|
|
143
|
+
"这个想法最想解决的具体痛点是什么?",
|
|
144
|
+
"第一个可验证的用户场景是什么?",
|
|
145
|
+
"什么样的最小成果能证明它值得继续投入?"
|
|
146
|
+
],
|
|
147
|
+
mvpPath: [
|
|
148
|
+
"把想法改写成一句清晰的问题陈述。",
|
|
149
|
+
"列出 1 个目标用户和 1 个高频使用场景。",
|
|
150
|
+
"产出一个最小原型、提纲或行动清单。"
|
|
151
|
+
],
|
|
152
|
+
risks: ["想法还停留在概念层,缺少明确使用场景。", "下一步过大时容易变成长期搁置的项目。"],
|
|
153
|
+
nextActions: [
|
|
154
|
+
{
|
|
155
|
+
kind: "chat",
|
|
156
|
+
text: "和 AI 继续深聊这个想法,收敛成问题陈述。"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
kind: "research",
|
|
160
|
+
text: "补充 3 个相似产品、案例或用户反馈。"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
kind: "task",
|
|
164
|
+
text: "写下今天能完成的一个最小推进动作。"
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
confidence: Number(confidence.toFixed(2))
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function stripJsonCodeFence(raw) {
|
|
171
|
+
return raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
172
|
+
}
|
|
173
|
+
function extractJsonObject(raw) {
|
|
174
|
+
if (!raw.trim()) return null;
|
|
175
|
+
const text = stripJsonCodeFence(raw);
|
|
176
|
+
try {
|
|
177
|
+
const data = JSON.parse(text);
|
|
178
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : null;
|
|
179
|
+
} catch {
|
|
180
|
+
const start = text.indexOf("{");
|
|
181
|
+
const end = text.lastIndexOf("}");
|
|
182
|
+
if (start < 0 || end <= start) return null;
|
|
183
|
+
try {
|
|
184
|
+
const data = JSON.parse(text.slice(start, end + 1));
|
|
185
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : null;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function extractAssistantText(result) {
|
|
192
|
+
if (!Array.isArray(result.content)) return "";
|
|
193
|
+
return result.content.filter((block) => {
|
|
194
|
+
return !!block && typeof block === "object" && block.type === "text";
|
|
195
|
+
}).map((block) => block.text).join("").trim();
|
|
196
|
+
}
|
|
197
|
+
function stringArray(value, fallback, limit) {
|
|
198
|
+
if (!Array.isArray(value)) return fallback;
|
|
199
|
+
const items = value.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean).slice(0, limit);
|
|
200
|
+
return items.length ? items : fallback;
|
|
201
|
+
}
|
|
202
|
+
function catalysisActions(value, fallback) {
|
|
203
|
+
if (!Array.isArray(value)) return fallback;
|
|
204
|
+
const allowed = new Set([
|
|
205
|
+
"task",
|
|
206
|
+
"workflow",
|
|
207
|
+
"research",
|
|
208
|
+
"share",
|
|
209
|
+
"chat"
|
|
210
|
+
]);
|
|
211
|
+
const items = [];
|
|
212
|
+
for (const item of value) {
|
|
213
|
+
if (!item || typeof item !== "object") continue;
|
|
214
|
+
const row = item;
|
|
215
|
+
const kind = typeof row.kind === "string" && allowed.has(row.kind) ? row.kind : "task";
|
|
216
|
+
const text = typeof row.text === "string" ? row.text.trim() : "";
|
|
217
|
+
if (text) items.push({
|
|
218
|
+
kind,
|
|
219
|
+
text
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return items.length ? items.slice(0, 5) : fallback;
|
|
223
|
+
}
|
|
224
|
+
function normalizeAiCatalysisReport(note, data) {
|
|
225
|
+
const fallback = buildCatalysisReport(note);
|
|
226
|
+
const confidenceRaw = typeof data.confidence === "number" ? data.confidence : fallback.confidence;
|
|
227
|
+
const confidence = Math.min(1, Math.max(0, confidenceRaw));
|
|
228
|
+
return {
|
|
229
|
+
originalNoteId: note.id,
|
|
230
|
+
generatedAt: Date.now(),
|
|
231
|
+
title: typeof data.title === "string" && data.title.trim() ? data.title.trim().slice(0, 80) : fallback.title,
|
|
232
|
+
valueHypothesis: typeof data.valueHypothesis === "string" && data.valueHypothesis.trim() ? data.valueHypothesis.trim() : fallback.valueHypothesis,
|
|
233
|
+
targetUsers: stringArray(data.targetUsers, fallback.targetUsers, 5),
|
|
234
|
+
keyQuestions: stringArray(data.keyQuestions, fallback.keyQuestions, 6),
|
|
235
|
+
mvpPath: stringArray(data.mvpPath, fallback.mvpPath, 6),
|
|
236
|
+
risks: stringArray(data.risks, fallback.risks, 5),
|
|
237
|
+
nextActions: catalysisActions(data.nextActions, fallback.nextActions),
|
|
238
|
+
confidence: Number(confidence.toFixed(2))
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function buildCatalysisPrompt(note) {
|
|
242
|
+
return `你是一个帮助用户把想法推进成成果的个人 AI Agent。请基于这条 Note 做“想法催化”,帮助用户进入:想法 → 创造 → 分享 → 反馈 的循环。\n\n要求:\n- 使用自然、具体的中文。\n- 不要空泛鼓励,要给出可执行路径。\n- 输出必须是单个 JSON 对象,不要 Markdown,不要代码块。\n- 字段必须包含:title, valueHypothesis, targetUsers, keyQuestions, mvpPath, risks, nextActions, confidence。\n- nextActions 每项格式为 {"kind":"task|workflow|research|share|chat","text":"..."}。\n- confidence 是 0 到 1 的数字。\n\nNote 标题:${note.title?.trim() || "未命名笔记"}\n\nNote 正文:\n${((note.text ?? "").trim() || "(无正文)").slice(0, 6e3)}`;
|
|
243
|
+
}
|
|
244
|
+
async function buildAiCatalysisReport(note, config) {
|
|
245
|
+
const data = extractJsonObject(extractAssistantText(await complete(resolveModel(getDefaultModelSync(config)), { messages: [{
|
|
246
|
+
role: "user",
|
|
247
|
+
content: buildCatalysisPrompt(note),
|
|
248
|
+
timestamp: Date.now()
|
|
249
|
+
}] }, {
|
|
250
|
+
maxTokens: 1800,
|
|
251
|
+
temperature: .2
|
|
252
|
+
})));
|
|
253
|
+
if (!data) throw new Error("Catalysis model did not return valid JSON");
|
|
254
|
+
return normalizeAiCatalysisReport(note, data);
|
|
255
|
+
}
|
|
106
256
|
const SNAPSHOT_THROTTLE_MS = 6e4;
|
|
107
257
|
const MAX_SNAPSHOTS_PER_NOTE = 30;
|
|
108
258
|
var NotesService = class {
|
|
@@ -121,6 +271,7 @@ var NotesService = class {
|
|
|
121
271
|
const blocks = noteTextToBlocks(text, id);
|
|
122
272
|
const note = {
|
|
123
273
|
id,
|
|
274
|
+
title: deriveDefaultTitle(text),
|
|
124
275
|
kind: inferKind(text),
|
|
125
276
|
status: "inbox",
|
|
126
277
|
text,
|
|
@@ -238,6 +389,92 @@ var NotesService = class {
|
|
|
238
389
|
}
|
|
239
390
|
};
|
|
240
391
|
}
|
|
392
|
+
async catalyzeNote(id, config) {
|
|
393
|
+
const note = await this.store.getNote(id);
|
|
394
|
+
if (!note) return null;
|
|
395
|
+
let report;
|
|
396
|
+
try {
|
|
397
|
+
report = await buildAiCatalysisReport(note, config);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
log.warn({
|
|
400
|
+
err,
|
|
401
|
+
noteId: id
|
|
402
|
+
}, "AI note catalysis failed; using local fallback report");
|
|
403
|
+
report = buildCatalysisReport(note);
|
|
404
|
+
}
|
|
405
|
+
const existingDeep = note.aiDeep;
|
|
406
|
+
const catalysis = {
|
|
407
|
+
...existingDeep?.catalysis,
|
|
408
|
+
status: "catalyzed",
|
|
409
|
+
stage: inferCatalysisStage(note),
|
|
410
|
+
lastCatalyzedAt: report.generatedAt,
|
|
411
|
+
confidence: report.confidence,
|
|
412
|
+
report
|
|
413
|
+
};
|
|
414
|
+
const updated = await this.updateNote(id, {
|
|
415
|
+
ai: {
|
|
416
|
+
...note.ai,
|
|
417
|
+
intent: note.ai?.intent ?? "idea",
|
|
418
|
+
summary: note.ai?.summary ?? report.valueHypothesis
|
|
419
|
+
},
|
|
420
|
+
aiDeep: {
|
|
421
|
+
...existingDeep,
|
|
422
|
+
processedAt: report.generatedAt,
|
|
423
|
+
insights: report.valueHypothesis,
|
|
424
|
+
catalysis
|
|
425
|
+
},
|
|
426
|
+
status: note.status === "inbox" ? "processed" : note.status
|
|
427
|
+
}, "ai_edit");
|
|
428
|
+
return updated ? {
|
|
429
|
+
note: updated,
|
|
430
|
+
report
|
|
431
|
+
} : null;
|
|
432
|
+
}
|
|
433
|
+
async recordCatalysisFeedback(id, feedback) {
|
|
434
|
+
const note = await this.store.getNote(id);
|
|
435
|
+
if (!note) return null;
|
|
436
|
+
const now = Date.now();
|
|
437
|
+
return this.updateNote(id, { aiDeep: {
|
|
438
|
+
...note.aiDeep,
|
|
439
|
+
processedAt: now,
|
|
440
|
+
catalysis: {
|
|
441
|
+
status: note.aiDeep?.catalysis?.status ?? "catalyzed",
|
|
442
|
+
...note.aiDeep?.catalysis,
|
|
443
|
+
feedback
|
|
444
|
+
}
|
|
445
|
+
} }, "ai_edit");
|
|
446
|
+
}
|
|
447
|
+
async linkNoteThread(id, sessionKey) {
|
|
448
|
+
const note = await this.store.getNote(id);
|
|
449
|
+
if (!note) return null;
|
|
450
|
+
const existingKeys = note.aiDeep?.catalysis?.linkedSessionKeys ?? [];
|
|
451
|
+
const linkedSessionKeys = Array.from(new Set([sessionKey, ...existingKeys]));
|
|
452
|
+
return this.updateNote(id, { aiDeep: {
|
|
453
|
+
...note.aiDeep,
|
|
454
|
+
processedAt: Date.now(),
|
|
455
|
+
catalysis: {
|
|
456
|
+
status: note.aiDeep?.catalysis?.status ?? "none",
|
|
457
|
+
...note.aiDeep?.catalysis,
|
|
458
|
+
sourceSessionKey: note.aiDeep?.catalysis?.sourceSessionKey ?? sessionKey,
|
|
459
|
+
linkedSessionKeys
|
|
460
|
+
}
|
|
461
|
+
} }, "sync");
|
|
462
|
+
}
|
|
463
|
+
async listNoteThreads(id) {
|
|
464
|
+
const note = await this.store.getNote(id);
|
|
465
|
+
if (!note) return null;
|
|
466
|
+
const keys = [note.aiDeep?.catalysis?.sourceSessionKey, ...note.aiDeep?.catalysis?.linkedSessionKeys ?? []].filter((key) => typeof key === "string" && key.length > 0);
|
|
467
|
+
return Array.from(new Set(keys));
|
|
468
|
+
}
|
|
469
|
+
async appendTextToNote(id, content, heading = "AI 讨论沉淀") {
|
|
470
|
+
const note = await this.store.getNote(id);
|
|
471
|
+
if (!note) return null;
|
|
472
|
+
const trimmed = content.trim();
|
|
473
|
+
if (!trimmed) return note;
|
|
474
|
+
const currentText = note.text?.trimEnd() ?? "";
|
|
475
|
+
const nextText = `${currentText}${currentText ? "\n\n" : ""}## ${heading}\n\n${trimmed}`;
|
|
476
|
+
return this.updateNote(id, { text: nextText }, "ai_edit");
|
|
477
|
+
}
|
|
241
478
|
async deleteNote(id) {
|
|
242
479
|
const deleted = await this.store.deleteNote(id);
|
|
243
480
|
if (deleted) {
|
|
@@ -305,6 +542,48 @@ var NotesService = class {
|
|
|
305
542
|
async flush() {
|
|
306
543
|
await this.store.flush();
|
|
307
544
|
}
|
|
545
|
+
async moveToGroup(noteId, groupId) {
|
|
546
|
+
return this.updateNote(noteId, { groupId: groupId ?? void 0 });
|
|
547
|
+
}
|
|
548
|
+
async createTask(title, source, options) {
|
|
549
|
+
return this.createNote({
|
|
550
|
+
title,
|
|
551
|
+
kind: "task",
|
|
552
|
+
capturedVia: source,
|
|
553
|
+
groupId: options?.groupId,
|
|
554
|
+
taskMeta: {
|
|
555
|
+
done: false,
|
|
556
|
+
dueAt: options?.dueAt,
|
|
557
|
+
priority: options?.priority,
|
|
558
|
+
sourceSessionKey: options?.sourceSessionKey,
|
|
559
|
+
sourceNoteId: options?.sourceNoteId
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
async toggleTaskDone(noteId) {
|
|
564
|
+
const note = await this.store.getNote(noteId);
|
|
565
|
+
if (!note || note.kind !== "task") return null;
|
|
566
|
+
const done = !note.taskMeta?.done;
|
|
567
|
+
return this.updateNote(noteId, {
|
|
568
|
+
taskMeta: {
|
|
569
|
+
...note.taskMeta,
|
|
570
|
+
done
|
|
571
|
+
},
|
|
572
|
+
status: done ? "archived" : "processed"
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
async updateTaskMeta(noteId, patch) {
|
|
576
|
+
const note = await this.store.getNote(noteId);
|
|
577
|
+
if (!note || note.kind !== "task") return null;
|
|
578
|
+
return this.updateNote(noteId, { taskMeta: {
|
|
579
|
+
...note.taskMeta,
|
|
580
|
+
done: note.taskMeta?.done ?? false,
|
|
581
|
+
...patch
|
|
582
|
+
} });
|
|
583
|
+
}
|
|
584
|
+
async recordOpen(noteId) {
|
|
585
|
+
return this.updateNote(noteId, { lastOpenedAt: Date.now() });
|
|
586
|
+
}
|
|
308
587
|
async maybeSaveSnapshot(note, trigger) {
|
|
309
588
|
if (trigger !== "edit") {
|
|
310
589
|
await this.store.saveSnapshot(note, trigger);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../../src/notes/service.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { createLogger } from '../utils/logger.js';\nimport { buildNoteAttachmentRef, attachmentIdFromTarget } from './attachment-ref.js';\nimport { partitionAttachmentsByReference } from './note-attachment-sync.js';\nimport { NotesStore } from './store.js';\nimport type {\n CaptureSource,\n CreateNoteParams,\n Note,\n NoteAiPatch,\n NoteAttachment,\n NoteBlock,\n NoteIndexEntry,\n NoteKind,\n NoteSnapshot,\n NoteSnapshotEntry,\n NotesListQuery,\n SnapshotTrigger,\n} from './types.js';\n\nconst log = createLogger('NotesService');\n\nfunction inferKind(\n text?: string,\n hasAttachments?: boolean,\n attachments?: NoteAttachment[],\n): NoteKind {\n if (hasAttachments && attachments?.length && attachments.every((item) => item.type === 'audio')) {\n return 'voice';\n }\n if (hasAttachments) return 'media';\n if (!text) return 'thought';\n const lower = text.toLowerCase();\n if (/^(todo|task|remind|buy|call|email|meet|finish|submit|send)\\b/i.test(lower) ||\n /\\b(明天|今天|记得|别忘|待办|提醒)\\b/.test(text)) {\n return 'todo';\n }\n if (/^https?:\\/\\//.test(text.trim())) return 'bookmark';\n return 'thought';\n}\n\nfunction createBlockId(): string {\n return `block_${Date.now()}_${randomUUID().slice(0, 8)}`;\n}\n\nconst IMAGE_MARKDOWN = /^!\\[([^\\]]*)\\]\\(([^)]+)\\)$/;\n\nfunction noteTextToBlocks(text?: string, noteId?: string): NoteBlock[] | undefined {\n if (!text?.trim()) return undefined;\n const now = Date.now();\n return text.split(/\\n{2,}/).map((part) => {\n const trimmed = part.trim();\n if (noteId) {\n const imageMatch = trimmed.match(IMAGE_MARKDOWN);\n if (imageMatch) {\n const attachmentId = attachmentIdFromTarget(imageMatch[2], noteId);\n if (attachmentId) {\n return {\n id: createBlockId(),\n type: 'image' as const,\n attachmentId,\n alt: imageMatch[1] || undefined,\n createdAt: now,\n updatedAt: now,\n };\n }\n }\n }\n return {\n id: createBlockId(),\n type: 'paragraph' as const,\n text: trimmed,\n createdAt: now,\n updatedAt: now,\n };\n });\n}\n\nfunction blocksToPlainText(blocks?: NoteBlock[], noteId?: string): string | undefined {\n if (!blocks?.length) return undefined;\n return blocks\n .map((block) => {\n if (block.type === 'divider') return '---';\n if (block.type === 'todo') return `${block.checked ? '[x]' : '[ ]'} ${block.text}`;\n if (block.type === 'image') {\n if (noteId) {\n return `})`;\n }\n return block.alt ?? '';\n }\n return block.text;\n })\n .filter((text) => text.trim().length > 0)\n .join('\\n\\n');\n}\n\nfunction createAiOrganizedBlocks(blocks: NoteBlock[], instruction: string): NoteBlock[] {\n const now = Date.now();\n const plainText = blocksToPlainText(blocks) || '';\n const lines = plainText\n .split(/\\n+/)\n .map((line) => line.trim())\n .filter(Boolean);\n const wantsTodos = /待办|todo|task|行动|提醒/i.test(instruction);\n const wantsSummary = /摘要|总结|summary|压缩/i.test(instruction);\n\n if (wantsTodos) {\n const candidates = lines.filter((line) => /要|需|记得|todo|task|完成|提交|联系|跟进|提醒/i.test(line));\n return (candidates.length ? candidates : lines).slice(0, 12).map((line) => ({\n id: createBlockId(),\n type: 'todo' as const,\n text: line.replace(/^[-*\\d.\\s\\[\\]x]+/i, '').trim(),\n checked: false,\n createdAt: now,\n updatedAt: now,\n }));\n }\n\n if (wantsSummary) {\n const summary = lines.join(' ').slice(0, 220);\n return [{\n id: createBlockId(),\n type: 'paragraph',\n text: summary,\n createdAt: now,\n updatedAt: now,\n }];\n }\n\n const titleText = lines[0]?.slice(0, 40) || '整理后的笔记';\n const bodyLines = lines.slice(1).length ? lines.slice(1) : lines;\n return [\n {\n id: createBlockId(),\n type: 'heading',\n text: titleText,\n level: 2,\n createdAt: now,\n updatedAt: now,\n },\n ...bodyLines.map((line) => ({\n id: createBlockId(),\n type: 'bulletList' as const,\n text: line.replace(/^[-*\\d.\\s]+/, '').trim(),\n indent: 0,\n createdAt: now,\n updatedAt: now,\n })),\n ];\n}\n\nconst SNAPSHOT_THROTTLE_MS = 60_000;\nconst MAX_SNAPSHOTS_PER_NOTE = 30;\n\nexport class NotesService {\n private store: NotesStore;\n private lastSnapshotAt = new Map<string, number>();\n\n constructor(store: NotesStore) {\n this.store = store;\n }\n\n async initialize(): Promise<void> {\n await this.store.initialize();\n log.debug('NotesService initialized');\n }\n\n async quickCapture(text: string, source: CaptureSource): Promise<Note> {\n const now = Date.now();\n const id = randomUUID();\n const blocks = noteTextToBlocks(text, id);\n const note: Note = {\n id,\n kind: inferKind(text),\n status: 'inbox',\n text,\n blocks,\n createdAt: now,\n updatedAt: now,\n capturedVia: source,\n localVersion: 0,\n remoteVersion: 1,\n };\n await this.store.addNote(note);\n log.debug({ id: note.id, kind: note.kind }, 'Quick capture');\n return note;\n }\n\n async createNote(params: CreateNoteParams): Promise<Note> {\n const now = Date.now();\n const id = randomUUID();\n const blocks = params.blocks ?? noteTextToBlocks(params.text, id);\n const text = params.text ?? blocksToPlainText(blocks, id);\n const note: Note = {\n id,\n title: params.title,\n kind: params.kind || inferKind(text),\n status: 'inbox',\n text,\n blocks,\n createdAt: now,\n updatedAt: now,\n capturedVia: params.capturedVia,\n tags: params.tags,\n pinned: params.pinned,\n localVersion: 0,\n remoteVersion: 1,\n };\n await this.store.addNote(note);\n log.debug({ id: note.id, kind: note.kind }, 'Note created');\n return note;\n }\n\n async getNote(id: string): Promise<Note | null> {\n return this.store.getNote(id);\n }\n\n async updateNote(id: string, patch: Partial<Note>, trigger: SnapshotTrigger = 'edit'): Promise<Note | null> {\n const existing = await this.store.getNote(id);\n if (!existing) return null;\n\n const contentTouched = patch.text !== undefined || patch.blocks !== undefined || patch.title !== undefined;\n if (contentTouched) {\n await this.maybeSaveSnapshot(existing, trigger);\n }\n\n const normalizedPatch: Partial<Note> = { ...patch };\n if (patch.blocks) {\n normalizedPatch.text = patch.text ?? blocksToPlainText(patch.blocks, existing.id);\n } else if (typeof patch.text === 'string') {\n normalizedPatch.blocks = patch.blocks ?? noteTextToBlocks(patch.text, existing.id);\n }\n normalizedPatch.remoteVersion = (existing.remoteVersion ?? 0) + 1;\n\n if (contentTouched) {\n const merged: Note = {\n ...existing,\n ...normalizedPatch,\n id: existing.id,\n createdAt: existing.createdAt,\n updatedAt: Date.now(),\n };\n const reconciled = await this.reconcileAttachments(merged);\n normalizedPatch.attachments = reconciled.attachments;\n normalizedPatch.kind = reconciled.kind;\n }\n\n return this.store.updateNote(id, normalizedPatch);\n }\n\n private async reconcileAttachments(note: Note): Promise<Note> {\n const { kept, removed } = partitionAttachmentsByReference(note);\n if (removed.length === 0) return note;\n\n for (const attachment of removed) {\n await this.store.deleteAttachmentFile(note.id, attachment.relativePath);\n }\n\n log.debug(\n { noteId: note.id, removedIds: removed.map((attachment) => attachment.id) },\n 'Pruned orphan note attachments',\n );\n\n const hasAttachments = kept.length > 0;\n return {\n ...note,\n attachments: hasAttachments ? kept : undefined,\n kind: inferKind(note.text, hasAttachments, kept),\n };\n }\n\n async syncNote(\n id: string,\n patch: Partial<Note>,\n baseRemoteVersion?: number,\n ): Promise<{ note: Note | null; conflict: boolean }> {\n const existing = await this.store.getNote(id);\n if (!existing) return { note: null, conflict: false };\n\n const currentRemoteVersion = existing.remoteVersion ?? 0;\n if (baseRemoteVersion !== undefined && baseRemoteVersion < currentRemoteVersion) {\n return { note: existing, conflict: true };\n }\n\n const updated = await this.updateNote(id, patch, 'sync');\n return { note: updated, conflict: false };\n }\n\n async createAiEditPatch(\n id: string,\n instruction: string,\n blocks?: NoteBlock[],\n ): Promise<{ message: string; patch: NoteAiPatch } | null> {\n const note = await this.store.getNote(id);\n if (!note) return null;\n\n const sourceBlocks = blocks?.length ? blocks : note.blocks ?? noteTextToBlocks(note.text, note.id) ?? [];\n const organizedBlocks = createAiOrganizedBlocks(sourceBlocks, instruction);\n const patch: NoteAiPatch = {\n id: randomUUID(),\n summary: `已根据「${instruction.slice(0, 40)}」生成可预览的块级整理建议`,\n operations: [{ type: 'replaceBlocks', blocks: organizedBlocks }],\n };\n\n return {\n message: 'AI edit patch generated',\n patch,\n };\n }\n\n async deleteNote(id: string): Promise<boolean> {\n const deleted = await this.store.deleteNote(id);\n if (deleted) {\n await this.store.deleteAllSnapshots(id);\n this.lastSnapshotAt.delete(id);\n }\n return deleted;\n }\n\n async listNotes(query: NotesListQuery = {}): Promise<{ items: NoteIndexEntry[]; total: number }> {\n return this.store.listNotes(query);\n }\n\n async addAttachment(\n noteId: string,\n file: { name: string; buffer: Buffer; mimeType: string; duration?: number },\n ): Promise<NoteAttachment | null> {\n const note = await this.store.getNote(noteId);\n if (!note) return null;\n\n const { relativePath, size } = await this.store.saveAttachment(noteId, file.name, file.buffer);\n\n const attachment: NoteAttachment = {\n id: randomUUID(),\n type: inferAttachmentType(file.mimeType),\n mimeType: file.mimeType,\n fileName: file.name,\n size,\n relativePath,\n duration: file.duration,\n };\n\n const attachments = [...(note.attachments || []), attachment];\n const kind: NoteKind =\n note.kind === 'thought' && attachment.type === 'audio'\n ? 'voice'\n : note.kind === 'thought'\n ? 'media'\n : note.kind;\n await this.store.updateNote(noteId, { attachments, kind });\n\n return attachment;\n }\n\n async getAttachmentPath(\n noteId: string,\n attachmentId: string,\n ): Promise<{ filePath: string; mimeType: string; fileName: string } | null> {\n const note = await this.store.getNote(noteId);\n if (!note) return null;\n\n const attachment = note.attachments?.find((a) => a.id === attachmentId);\n if (!attachment) return null;\n\n const fullPath = this.store.resolveAttachmentPath(noteId, attachment.relativePath);\n return { filePath: fullPath, mimeType: attachment.mimeType, fileName: attachment.fileName };\n }\n\n async listNoteHistory(noteId: string): Promise<NoteSnapshotEntry[]> {\n return this.store.listSnapshots(noteId);\n }\n\n async getNoteSnapshot(noteId: string, timestamp: number): Promise<NoteSnapshot | null> {\n return this.store.getSnapshot(noteId, timestamp);\n }\n\n async restoreNoteSnapshot(noteId: string, timestamp: number): Promise<Note | null> {\n const snapshot = await this.store.getSnapshot(noteId, timestamp);\n if (!snapshot) return null;\n const existing = await this.store.getNote(noteId);\n if (!existing) return null;\n\n await this.store.saveSnapshot(existing, 'restore');\n this.lastSnapshotAt.set(noteId, Date.now());\n await this.store.pruneSnapshots(noteId, MAX_SNAPSHOTS_PER_NOTE);\n\n return this.store.updateNote(noteId, {\n title: snapshot.title,\n text: snapshot.text,\n blocks: snapshot.blocks,\n tags: snapshot.tags,\n });\n }\n\n async flush(): Promise<void> {\n await this.store.flush();\n }\n\n private async maybeSaveSnapshot(note: Note, trigger: SnapshotTrigger): Promise<void> {\n if (trigger !== 'edit') {\n await this.store.saveSnapshot(note, trigger);\n this.lastSnapshotAt.set(note.id, Date.now());\n await this.store.pruneSnapshots(note.id, MAX_SNAPSHOTS_PER_NOTE);\n return;\n }\n const last = this.lastSnapshotAt.get(note.id) ?? 0;\n if (Date.now() - last < SNAPSHOT_THROTTLE_MS) return;\n await this.store.saveSnapshot(note, trigger);\n this.lastSnapshotAt.set(note.id, Date.now());\n await this.store.pruneSnapshots(note.id, MAX_SNAPSHOTS_PER_NOTE);\n }\n}\n\nfunction inferAttachmentType(mimeType: string): NoteAttachment['type'] {\n if (mimeType.startsWith('image/')) return 'image';\n if (mimeType.startsWith('video/')) return 'video';\n if (mimeType.startsWith('audio/')) return 'audio';\n return 'file';\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../../src/notes/service.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { complete, type UserMessage } from '@earendil-works/pi-ai';\n\nimport type { Config } from '../config/schema.js';\nimport { getDefaultModelSync, resolveModel } from '../providers/index.js';\nimport { createLogger } from '../utils/logger.js';\nimport { buildNoteAttachmentRef, attachmentIdFromTarget } from './attachment-ref.js';\nimport { partitionAttachmentsByReference } from './note-attachment-sync.js';\nimport { NotesStore } from './store.js';\nimport type {\n CaptureSource,\n CreateNoteParams,\n Note,\n NoteAiPatch,\n NoteAttachment,\n NoteBlock,\n NoteCatalysisAction,\n NoteCatalysisMeta,\n NoteCatalysisReport,\n NoteIndexEntry,\n NoteKind,\n NoteSnapshot,\n NoteSnapshotEntry,\n NotesListQuery,\n SnapshotTrigger,\n} from './types.js';\n\nconst log = createLogger('NotesService');\n\nfunction inferKind(\n text?: string,\n hasAttachments?: boolean,\n attachments?: NoteAttachment[],\n): NoteKind {\n if (hasAttachments && attachments?.length && attachments.every((item) => item.type === 'audio')) {\n return 'voice';\n }\n if (hasAttachments) return 'media';\n if (!text) return 'thought';\n const lower = text.toLowerCase();\n if (/^(todo|task|remind|buy|call|email|meet|finish|submit|send)\\b/i.test(lower) ||\n /\\b(明天|今天|记得|别忘|待办|提醒)\\b/.test(text)) {\n return 'todo';\n }\n if (/^https?:\\/\\//.test(text.trim())) return 'bookmark';\n return 'thought';\n}\n\nfunction deriveDefaultTitle(text?: string): string | undefined {\n const normalizedText = text?.trim().replace(/\\s+/g, ' ');\n if (!normalizedText) return undefined;\n return Array.from(normalizedText).slice(0, 10).join('');\n}\n\nfunction createBlockId(): string {\n return `block_${Date.now()}_${randomUUID().slice(0, 8)}`;\n}\n\nconst IMAGE_MARKDOWN = /^!\\[([^\\]]*)\\]\\(([^)]+)\\)$/;\n\nfunction noteTextToBlocks(text?: string, noteId?: string): NoteBlock[] | undefined {\n if (!text?.trim()) return undefined;\n const now = Date.now();\n return text.split(/\\n{2,}/).map((part) => {\n const trimmed = part.trim();\n if (noteId) {\n const imageMatch = trimmed.match(IMAGE_MARKDOWN);\n if (imageMatch) {\n const attachmentId = attachmentIdFromTarget(imageMatch[2], noteId);\n if (attachmentId) {\n return {\n id: createBlockId(),\n type: 'image' as const,\n attachmentId,\n alt: imageMatch[1] || undefined,\n createdAt: now,\n updatedAt: now,\n };\n }\n }\n }\n return {\n id: createBlockId(),\n type: 'paragraph' as const,\n text: trimmed,\n createdAt: now,\n updatedAt: now,\n };\n });\n}\n\nfunction blocksToPlainText(blocks?: NoteBlock[], noteId?: string): string | undefined {\n if (!blocks?.length) return undefined;\n return blocks\n .map((block) => {\n if (block.type === 'divider') return '---';\n if (block.type === 'todo') return `${block.checked ? '[x]' : '[ ]'} ${block.text}`;\n if (block.type === 'image') {\n if (noteId) {\n return `})`;\n }\n return block.alt ?? '';\n }\n return block.text;\n })\n .filter((text) => text.trim().length > 0)\n .join('\\n\\n');\n}\n\nfunction createAiOrganizedBlocks(blocks: NoteBlock[], instruction: string): NoteBlock[] {\n const now = Date.now();\n const plainText = blocksToPlainText(blocks) || '';\n const lines = plainText\n .split(/\\n+/)\n .map((line) => line.trim())\n .filter(Boolean);\n const wantsTodos = /待办|todo|task|行动|提醒/i.test(instruction);\n const wantsSummary = /摘要|总结|summary|压缩/i.test(instruction);\n\n if (wantsTodos) {\n const candidates = lines.filter((line) => /要|需|记得|todo|task|完成|提交|联系|跟进|提醒/i.test(line));\n return (candidates.length ? candidates : lines).slice(0, 12).map((line) => ({\n id: createBlockId(),\n type: 'todo' as const,\n text: line.replace(/^[-*\\d.\\s\\[\\]x]+/i, '').trim(),\n checked: false,\n createdAt: now,\n updatedAt: now,\n }));\n }\n\n if (wantsSummary) {\n const summary = lines.join(' ').slice(0, 220);\n return [{\n id: createBlockId(),\n type: 'paragraph',\n text: summary,\n createdAt: now,\n updatedAt: now,\n }];\n }\n\n const titleText = lines[0]?.slice(0, 40) || '整理后的笔记';\n const bodyLines = lines.slice(1).length ? lines.slice(1) : lines;\n return [\n {\n id: createBlockId(),\n type: 'heading',\n text: titleText,\n level: 2,\n createdAt: now,\n updatedAt: now,\n },\n ...bodyLines.map((line) => ({\n id: createBlockId(),\n type: 'bulletList' as const,\n text: line.replace(/^[-*\\d.\\s]+/, '').trim(),\n indent: 0,\n createdAt: now,\n updatedAt: now,\n })),\n ];\n}\n\nfunction splitMeaningfulLines(text?: string): string[] {\n return (text ?? '')\n .split(/[\\n。!?!?;;]+/)\n .map((line) => line.trim().replace(/^[-*\\d.\\s]+/, ''))\n .filter((line) => line.length > 0);\n}\n\nfunction summarizeIdea(text?: string): string {\n const lines = splitMeaningfulLines(text);\n const joined = lines.join(' ');\n return Array.from(joined || '这个想法').slice(0, 90).join('');\n}\n\nfunction inferCatalysisStage(note: Note): NonNullable<NoteCatalysisMeta['stage']> {\n const text = `${note.title ?? ''}\\n${note.text ?? ''}`;\n if (/发布|上线|分享|推广|launch|ship/i.test(text)) return 'shipped';\n if (/验证|实验|用户|指标|反馈|validate|experiment/i.test(text)) return 'validating';\n if (/实现|开发|MVP|原型|prototype|build/i.test(text)) return 'developing';\n if (text.trim().length > 80) return 'incubating';\n return 'seed';\n}\n\nfunction buildCatalysisReport(note: Note): NoteCatalysisReport {\n const generatedAt = Date.now();\n const summary = summarizeIdea(note.text ?? note.title);\n const title = note.title?.trim() || summary.slice(0, 28) || '未命名想法';\n const hasUserSignal = /用户|客户|读者|创作者|团队|个人|开发者|founder|creator|user/i.test(summary);\n const hasProductSignal = /产品|工具|平台|workflow|agent|AI|自动|系统|应用/i.test(summary);\n const confidence = Math.min(0.86, Math.max(0.42, 0.48 + (hasUserSignal ? 0.16 : 0) + (hasProductSignal ? 0.18 : 0) + Math.min(summary.length, 120) / 600));\n\n return {\n originalNoteId: note.id,\n generatedAt,\n title,\n valueHypothesis: `如果把「${summary}」推进成一个可体验的小成果,它最可能的价值是帮助用户更快完成判断、表达或行动。`,\n targetUsers: hasUserSignal ? ['笔记作者自己', '有类似场景的目标用户'] : ['笔记作者自己', '未来可能被这个想法帮助的人'],\n keyQuestions: [\n '这个想法最想解决的具体痛点是什么?',\n '第一个可验证的用户场景是什么?',\n '什么样的最小成果能证明它值得继续投入?',\n ],\n mvpPath: [\n '把想法改写成一句清晰的问题陈述。',\n '列出 1 个目标用户和 1 个高频使用场景。',\n '产出一个最小原型、提纲或行动清单。',\n ],\n risks: [\n '想法还停留在概念层,缺少明确使用场景。',\n '下一步过大时容易变成长期搁置的项目。',\n ],\n nextActions: [\n { kind: 'chat', text: '和 AI 继续深聊这个想法,收敛成问题陈述。' },\n { kind: 'research', text: '补充 3 个相似产品、案例或用户反馈。' },\n { kind: 'task', text: '写下今天能完成的一个最小推进动作。' },\n ],\n confidence: Number(confidence.toFixed(2)),\n };\n}\n\nfunction stripJsonCodeFence(raw: string): string {\n return raw.trim().replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/i, '').trim();\n}\n\nfunction extractJsonObject(raw: string): Record<string, unknown> | null {\n if (!raw.trim()) return null;\n const text = stripJsonCodeFence(raw);\n try {\n const data = JSON.parse(text) as unknown;\n return data && typeof data === 'object' && !Array.isArray(data) ? data as Record<string, unknown> : null;\n } catch {\n const start = text.indexOf('{');\n const end = text.lastIndexOf('}');\n if (start < 0 || end <= start) return null;\n try {\n const data = JSON.parse(text.slice(start, end + 1)) as unknown;\n return data && typeof data === 'object' && !Array.isArray(data) ? data as Record<string, unknown> : null;\n } catch {\n return null;\n }\n }\n}\n\nfunction extractAssistantText(result: { content?: unknown }): string {\n if (!Array.isArray(result.content)) return '';\n return result.content\n .filter((block): block is { type: string; text: string } => {\n return !!block && typeof block === 'object' && (block as { type?: string }).type === 'text';\n })\n .map((block) => block.text)\n .join('')\n .trim();\n}\n\nfunction stringArray(value: unknown, fallback: string[], limit: number): string[] {\n if (!Array.isArray(value)) return fallback;\n const items = value\n .map((item) => typeof item === 'string' ? item.trim() : '')\n .filter(Boolean)\n .slice(0, limit);\n return items.length ? items : fallback;\n}\n\nfunction catalysisActions(value: unknown, fallback: NoteCatalysisAction[]): NoteCatalysisAction[] {\n if (!Array.isArray(value)) return fallback;\n const allowed = new Set<NoteCatalysisAction['kind']>(['task', 'workflow', 'research', 'share', 'chat']);\n const items: NoteCatalysisAction[] = [];\n for (const item of value) {\n if (!item || typeof item !== 'object') continue;\n const row = item as { kind?: unknown; text?: unknown };\n const kind = typeof row.kind === 'string' && allowed.has(row.kind as NoteCatalysisAction['kind'])\n ? row.kind as NoteCatalysisAction['kind']\n : 'task';\n const text = typeof row.text === 'string' ? row.text.trim() : '';\n if (text) items.push({ kind, text });\n }\n return items.length ? items.slice(0, 5) : fallback;\n}\n\nfunction normalizeAiCatalysisReport(note: Note, data: Record<string, unknown>): NoteCatalysisReport {\n const fallback = buildCatalysisReport(note);\n const confidenceRaw = typeof data.confidence === 'number' ? data.confidence : fallback.confidence;\n const confidence = Math.min(1, Math.max(0, confidenceRaw));\n return {\n originalNoteId: note.id,\n generatedAt: Date.now(),\n title: typeof data.title === 'string' && data.title.trim() ? data.title.trim().slice(0, 80) : fallback.title,\n valueHypothesis: typeof data.valueHypothesis === 'string' && data.valueHypothesis.trim()\n ? data.valueHypothesis.trim()\n : fallback.valueHypothesis,\n targetUsers: stringArray(data.targetUsers, fallback.targetUsers, 5),\n keyQuestions: stringArray(data.keyQuestions, fallback.keyQuestions, 6),\n mvpPath: stringArray(data.mvpPath, fallback.mvpPath, 6),\n risks: stringArray(data.risks, fallback.risks, 5),\n nextActions: catalysisActions(data.nextActions, fallback.nextActions),\n confidence: Number(confidence.toFixed(2)),\n };\n}\n\nfunction buildCatalysisPrompt(note: Note): string {\n const title = note.title?.trim() || '未命名笔记';\n const text = (note.text ?? '').trim() || '(无正文)';\n return `你是一个帮助用户把想法推进成成果的个人 AI Agent。请基于这条 Note 做“想法催化”,帮助用户进入:想法 → 创造 → 分享 → 反馈 的循环。\\n\\n要求:\\n- 使用自然、具体的中文。\\n- 不要空泛鼓励,要给出可执行路径。\\n- 输出必须是单个 JSON 对象,不要 Markdown,不要代码块。\\n- 字段必须包含:title, valueHypothesis, targetUsers, keyQuestions, mvpPath, risks, nextActions, confidence。\\n- nextActions 每项格式为 {\"kind\":\"task|workflow|research|share|chat\",\"text\":\"...\"}。\\n- confidence 是 0 到 1 的数字。\\n\\nNote 标题:${title}\\n\\nNote 正文:\\n${text.slice(0, 6000)}`;\n}\n\nasync function buildAiCatalysisReport(note: Note, config?: Config): Promise<NoteCatalysisReport> {\n const model = resolveModel(getDefaultModelSync(config));\n const user: UserMessage = {\n role: 'user',\n content: buildCatalysisPrompt(note),\n timestamp: Date.now(),\n };\n const result = await complete(\n model,\n { messages: [user] },\n { maxTokens: 1800, temperature: 0.2 },\n );\n const text = extractAssistantText(result);\n const data = extractJsonObject(text);\n if (!data) {\n throw new Error('Catalysis model did not return valid JSON');\n }\n return normalizeAiCatalysisReport(note, data);\n}\n\nconst SNAPSHOT_THROTTLE_MS = 60_000;\nconst MAX_SNAPSHOTS_PER_NOTE = 30;\n\nexport class NotesService {\n private store: NotesStore;\n private lastSnapshotAt = new Map<string, number>();\n\n constructor(store: NotesStore) {\n this.store = store;\n }\n\n async initialize(): Promise<void> {\n await this.store.initialize();\n log.debug('NotesService initialized');\n }\n\n async quickCapture(text: string, source: CaptureSource): Promise<Note> {\n const now = Date.now();\n const id = randomUUID();\n const blocks = noteTextToBlocks(text, id);\n const note: Note = {\n id,\n title: deriveDefaultTitle(text),\n kind: inferKind(text),\n status: 'inbox',\n text,\n blocks,\n createdAt: now,\n updatedAt: now,\n capturedVia: source,\n localVersion: 0,\n remoteVersion: 1,\n };\n await this.store.addNote(note);\n log.debug({ id: note.id, kind: note.kind }, 'Quick capture');\n return note;\n }\n\n async createNote(params: CreateNoteParams): Promise<Note> {\n const now = Date.now();\n const id = randomUUID();\n const blocks = params.blocks ?? noteTextToBlocks(params.text, id);\n const text = params.text ?? blocksToPlainText(blocks, id);\n const note: Note = {\n id,\n title: params.title,\n kind: params.kind || inferKind(text),\n status: 'inbox',\n text,\n blocks,\n createdAt: now,\n updatedAt: now,\n capturedVia: params.capturedVia,\n tags: params.tags,\n pinned: params.pinned,\n localVersion: 0,\n remoteVersion: 1,\n };\n await this.store.addNote(note);\n log.debug({ id: note.id, kind: note.kind }, 'Note created');\n return note;\n }\n\n async getNote(id: string): Promise<Note | null> {\n return this.store.getNote(id);\n }\n\n async updateNote(id: string, patch: Partial<Note>, trigger: SnapshotTrigger = 'edit'): Promise<Note | null> {\n const existing = await this.store.getNote(id);\n if (!existing) return null;\n\n const contentTouched = patch.text !== undefined || patch.blocks !== undefined || patch.title !== undefined;\n if (contentTouched) {\n await this.maybeSaveSnapshot(existing, trigger);\n }\n\n const normalizedPatch: Partial<Note> = { ...patch };\n if (patch.blocks) {\n normalizedPatch.text = patch.text ?? blocksToPlainText(patch.blocks, existing.id);\n } else if (typeof patch.text === 'string') {\n normalizedPatch.blocks = patch.blocks ?? noteTextToBlocks(patch.text, existing.id);\n }\n normalizedPatch.remoteVersion = (existing.remoteVersion ?? 0) + 1;\n\n if (contentTouched) {\n const merged: Note = {\n ...existing,\n ...normalizedPatch,\n id: existing.id,\n createdAt: existing.createdAt,\n updatedAt: Date.now(),\n };\n const reconciled = await this.reconcileAttachments(merged);\n normalizedPatch.attachments = reconciled.attachments;\n normalizedPatch.kind = reconciled.kind;\n }\n\n return this.store.updateNote(id, normalizedPatch);\n }\n\n private async reconcileAttachments(note: Note): Promise<Note> {\n const { kept, removed } = partitionAttachmentsByReference(note);\n if (removed.length === 0) return note;\n\n for (const attachment of removed) {\n await this.store.deleteAttachmentFile(note.id, attachment.relativePath);\n }\n\n log.debug(\n { noteId: note.id, removedIds: removed.map((attachment) => attachment.id) },\n 'Pruned orphan note attachments',\n );\n\n const hasAttachments = kept.length > 0;\n return {\n ...note,\n attachments: hasAttachments ? kept : undefined,\n kind: inferKind(note.text, hasAttachments, kept),\n };\n }\n\n async syncNote(\n id: string,\n patch: Partial<Note>,\n baseRemoteVersion?: number,\n ): Promise<{ note: Note | null; conflict: boolean }> {\n const existing = await this.store.getNote(id);\n if (!existing) return { note: null, conflict: false };\n\n const currentRemoteVersion = existing.remoteVersion ?? 0;\n if (baseRemoteVersion !== undefined && baseRemoteVersion < currentRemoteVersion) {\n return { note: existing, conflict: true };\n }\n\n const updated = await this.updateNote(id, patch, 'sync');\n return { note: updated, conflict: false };\n }\n\n async createAiEditPatch(\n id: string,\n instruction: string,\n blocks?: NoteBlock[],\n ): Promise<{ message: string; patch: NoteAiPatch } | null> {\n const note = await this.store.getNote(id);\n if (!note) return null;\n\n const sourceBlocks = blocks?.length ? blocks : note.blocks ?? noteTextToBlocks(note.text, note.id) ?? [];\n const organizedBlocks = createAiOrganizedBlocks(sourceBlocks, instruction);\n const patch: NoteAiPatch = {\n id: randomUUID(),\n summary: `已根据「${instruction.slice(0, 40)}」生成可预览的块级整理建议`,\n operations: [{ type: 'replaceBlocks', blocks: organizedBlocks }],\n };\n\n return {\n message: 'AI edit patch generated',\n patch,\n };\n }\n\n async catalyzeNote(id: string, config?: Config): Promise<{ note: Note; report: NoteCatalysisReport } | null> {\n const note = await this.store.getNote(id);\n if (!note) return null;\n\n let report: NoteCatalysisReport;\n try {\n report = await buildAiCatalysisReport(note, config);\n } catch (err) {\n log.warn({ err, noteId: id }, 'AI note catalysis failed; using local fallback report');\n report = buildCatalysisReport(note);\n }\n\n const existingDeep = note.aiDeep;\n const catalysis: NoteCatalysisMeta = {\n ...existingDeep?.catalysis,\n status: 'catalyzed',\n stage: inferCatalysisStage(note),\n lastCatalyzedAt: report.generatedAt,\n confidence: report.confidence,\n report,\n };\n\n const updated = await this.updateNote(id, {\n ai: {\n ...note.ai,\n intent: note.ai?.intent ?? 'idea',\n summary: note.ai?.summary ?? report.valueHypothesis,\n },\n aiDeep: {\n ...existingDeep,\n processedAt: report.generatedAt,\n insights: report.valueHypothesis,\n catalysis,\n },\n status: note.status === 'inbox' ? 'processed' : note.status,\n }, 'ai_edit');\n\n return updated ? { note: updated, report } : null;\n }\n\n async recordCatalysisFeedback(\n id: string,\n feedback: NonNullable<NoteCatalysisMeta['feedback']>,\n ): Promise<Note | null> {\n const note = await this.store.getNote(id);\n if (!note) return null;\n const now = Date.now();\n return this.updateNote(id, {\n aiDeep: {\n ...note.aiDeep,\n processedAt: now,\n catalysis: {\n status: note.aiDeep?.catalysis?.status ?? 'catalyzed',\n ...note.aiDeep?.catalysis,\n feedback,\n },\n },\n }, 'ai_edit');\n }\n\n async linkNoteThread(id: string, sessionKey: string): Promise<Note | null> {\n const note = await this.store.getNote(id);\n if (!note) return null;\n const existingKeys = note.aiDeep?.catalysis?.linkedSessionKeys ?? [];\n const linkedSessionKeys = Array.from(new Set([sessionKey, ...existingKeys]));\n return this.updateNote(id, {\n aiDeep: {\n ...note.aiDeep,\n processedAt: Date.now(),\n catalysis: {\n status: note.aiDeep?.catalysis?.status ?? 'none',\n ...note.aiDeep?.catalysis,\n sourceSessionKey: note.aiDeep?.catalysis?.sourceSessionKey ?? sessionKey,\n linkedSessionKeys,\n },\n },\n }, 'sync');\n }\n\n async listNoteThreads(id: string): Promise<string[] | null> {\n const note = await this.store.getNote(id);\n if (!note) return null;\n const keys = [\n note.aiDeep?.catalysis?.sourceSessionKey,\n ...(note.aiDeep?.catalysis?.linkedSessionKeys ?? []),\n ].filter((key): key is string => typeof key === 'string' && key.length > 0);\n return Array.from(new Set(keys));\n }\n\n async appendTextToNote(id: string, content: string, heading = 'AI 讨论沉淀'): Promise<Note | null> {\n const note = await this.store.getNote(id);\n if (!note) return null;\n const trimmed = content.trim();\n if (!trimmed) return note;\n const currentText = note.text?.trimEnd() ?? '';\n const nextText = `${currentText}${currentText ? '\\n\\n' : ''}## ${heading}\\n\\n${trimmed}`;\n return this.updateNote(id, { text: nextText }, 'ai_edit');\n }\n\n async deleteNote(id: string): Promise<boolean> {\n const deleted = await this.store.deleteNote(id);\n if (deleted) {\n await this.store.deleteAllSnapshots(id);\n this.lastSnapshotAt.delete(id);\n }\n return deleted;\n }\n\n async listNotes(query: NotesListQuery = {}): Promise<{ items: NoteIndexEntry[]; total: number; limit: number; offset: number; hasMore: boolean }> {\n return this.store.listNotes(query);\n }\n\n async addAttachment(\n noteId: string,\n file: { name: string; buffer: Buffer; mimeType: string; duration?: number },\n ): Promise<NoteAttachment | null> {\n const note = await this.store.getNote(noteId);\n if (!note) return null;\n\n const { relativePath, size } = await this.store.saveAttachment(noteId, file.name, file.buffer);\n\n const attachment: NoteAttachment = {\n id: randomUUID(),\n type: inferAttachmentType(file.mimeType),\n mimeType: file.mimeType,\n fileName: file.name,\n size,\n relativePath,\n duration: file.duration,\n };\n\n const attachments = [...(note.attachments || []), attachment];\n const kind: NoteKind =\n note.kind === 'thought' && attachment.type === 'audio'\n ? 'voice'\n : note.kind === 'thought'\n ? 'media'\n : note.kind;\n await this.store.updateNote(noteId, { attachments, kind });\n\n return attachment;\n }\n\n async getAttachmentPath(\n noteId: string,\n attachmentId: string,\n ): Promise<{ filePath: string; mimeType: string; fileName: string } | null> {\n const note = await this.store.getNote(noteId);\n if (!note) return null;\n\n const attachment = note.attachments?.find((a) => a.id === attachmentId);\n if (!attachment) return null;\n\n const fullPath = this.store.resolveAttachmentPath(noteId, attachment.relativePath);\n return { filePath: fullPath, mimeType: attachment.mimeType, fileName: attachment.fileName };\n }\n\n async listNoteHistory(noteId: string): Promise<NoteSnapshotEntry[]> {\n return this.store.listSnapshots(noteId);\n }\n\n async getNoteSnapshot(noteId: string, timestamp: number): Promise<NoteSnapshot | null> {\n return this.store.getSnapshot(noteId, timestamp);\n }\n\n async restoreNoteSnapshot(noteId: string, timestamp: number): Promise<Note | null> {\n const snapshot = await this.store.getSnapshot(noteId, timestamp);\n if (!snapshot) return null;\n const existing = await this.store.getNote(noteId);\n if (!existing) return null;\n\n await this.store.saveSnapshot(existing, 'restore');\n this.lastSnapshotAt.set(noteId, Date.now());\n await this.store.pruneSnapshots(noteId, MAX_SNAPSHOTS_PER_NOTE);\n\n return this.store.updateNote(noteId, {\n title: snapshot.title,\n text: snapshot.text,\n blocks: snapshot.blocks,\n tags: snapshot.tags,\n });\n }\n\n async flush(): Promise<void> {\n await this.store.flush();\n }\n\n // ── Space grouping ──────────────────────────────────────────────────\n\n async moveToGroup(noteId: string, groupId: string | null): Promise<Note | null> {\n return this.updateNote(noteId, { groupId: groupId ?? undefined });\n }\n\n // ── Task lifecycle ──────────────────────────────────────────────────\n\n async createTask(\n title: string,\n source: CaptureSource,\n options?: { dueAt?: number; priority?: 'high' | 'medium' | 'low'; sourceSessionKey?: string; sourceNoteId?: string; groupId?: string },\n ): Promise<Note> {\n return this.createNote({\n title,\n kind: 'task',\n capturedVia: source,\n groupId: options?.groupId,\n taskMeta: {\n done: false,\n dueAt: options?.dueAt,\n priority: options?.priority,\n sourceSessionKey: options?.sourceSessionKey,\n sourceNoteId: options?.sourceNoteId,\n },\n });\n }\n\n async toggleTaskDone(noteId: string): Promise<Note | null> {\n const note = await this.store.getNote(noteId);\n if (!note || note.kind !== 'task') return null;\n const done = !note.taskMeta?.done;\n return this.updateNote(noteId, {\n taskMeta: { ...note.taskMeta, done },\n status: done ? 'archived' : 'processed',\n });\n }\n\n async updateTaskMeta(noteId: string, patch: Partial<import('./types.js').NoteTaskMeta>): Promise<Note | null> {\n const note = await this.store.getNote(noteId);\n if (!note || note.kind !== 'task') return null;\n return this.updateNote(noteId, {\n taskMeta: { ...note.taskMeta, done: note.taskMeta?.done ?? false, ...patch },\n });\n }\n\n // ── Open tracking ──────────────────────────────────────────────────\n\n async recordOpen(noteId: string): Promise<Note | null> {\n return this.updateNote(noteId, { lastOpenedAt: Date.now() } as Partial<Note>);\n }\n\n private async maybeSaveSnapshot(note: Note, trigger: SnapshotTrigger): Promise<void> {\n if (trigger !== 'edit') {\n await this.store.saveSnapshot(note, trigger);\n this.lastSnapshotAt.set(note.id, Date.now());\n await this.store.pruneSnapshots(note.id, MAX_SNAPSHOTS_PER_NOTE);\n return;\n }\n const last = this.lastSnapshotAt.get(note.id) ?? 0;\n if (Date.now() - last < SNAPSHOT_THROTTLE_MS) return;\n await this.store.saveSnapshot(note, trigger);\n this.lastSnapshotAt.set(note.id, Date.now());\n await this.store.pruneSnapshots(note.id, MAX_SNAPSHOTS_PER_NOTE);\n }\n}\n\nfunction inferAttachmentType(mimeType: string): NoteAttachment['type'] {\n if (mimeType.startsWith('image/')) return 'image';\n if (mimeType.startsWith('video/')) return 'video';\n if (mimeType.startsWith('audio/')) return 'audio';\n return 'file';\n}\n"],"mappings":"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"}
|
|
@@ -12,7 +12,11 @@ export declare class NotesStore {
|
|
|
12
12
|
listNotes(query?: NotesListQuery): Promise<{
|
|
13
13
|
items: NoteIndexEntry[];
|
|
14
14
|
total: number;
|
|
15
|
+
limit: number;
|
|
16
|
+
offset: number;
|
|
17
|
+
hasMore: boolean;
|
|
15
18
|
}>;
|
|
19
|
+
private noteIndexEntryMatchesSearch;
|
|
16
20
|
saveAttachment(noteId: string, fileName: string, buffer: Buffer): Promise<{
|
|
17
21
|
relativePath: string;
|
|
18
22
|
size: number;
|
package/dist/src/notes/store.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { init_write_file_atomic, writeTextAtomic } from "../infra/write-file-atomic.js";
|
|
1
2
|
import { createLogger } from "../utils/logger/index.js";
|
|
2
3
|
import { init_logger } from "../utils/logger.js";
|
|
3
|
-
import {
|
|
4
|
-
import { buildNoteIndexMeta } from "./note-index-meta.js";
|
|
4
|
+
import { buildNoteIndexMeta, notePlainText } from "./note-index-meta.js";
|
|
5
5
|
import { resolveNoteHistoryDir, resolveNoteItemPath, resolveNoteMediaDir, resolveNotesDir, resolveNotesIndexPath } from "./paths.js";
|
|
6
|
-
import { join } from "node:path";
|
|
7
6
|
import { randomUUID } from "node:crypto";
|
|
8
7
|
import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
9
|
//#region src/notes/store.ts
|
|
10
10
|
init_write_file_atomic();
|
|
11
11
|
init_logger();
|
|
@@ -31,7 +31,11 @@ function noteToIndexEntry(note) {
|
|
|
31
31
|
coverAttachmentId,
|
|
32
32
|
voiceAttachmentId,
|
|
33
33
|
voiceDurationSec,
|
|
34
|
-
attachmentNames
|
|
34
|
+
attachmentNames,
|
|
35
|
+
groupId: note.groupId || void 0,
|
|
36
|
+
lastOpenedAt: note.lastOpenedAt || void 0,
|
|
37
|
+
taskDone: note.taskMeta?.done,
|
|
38
|
+
taskDueAt: note.taskMeta?.dueAt
|
|
35
39
|
};
|
|
36
40
|
}
|
|
37
41
|
var NotesStore = class {
|
|
@@ -118,21 +122,46 @@ var NotesStore = class {
|
|
|
118
122
|
if (query.kind) results = results.filter((n) => n.kind === query.kind);
|
|
119
123
|
if (query.tag) results = results.filter((n) => n.tags?.includes(query.tag));
|
|
120
124
|
if (query.pinned !== void 0) results = results.filter((n) => Boolean(n.pinned) === query.pinned);
|
|
125
|
+
if (query.groupId !== void 0) if (query.groupId === "ungrouped") results = results.filter((n) => !n.groupId);
|
|
126
|
+
else results = results.filter((n) => n.groupId === query.groupId);
|
|
127
|
+
if (query.pendingTasksOnly) results = results.filter((n) => n.kind === "task" && !n.taskDone);
|
|
121
128
|
if (query.search) {
|
|
122
129
|
const term = query.search.toLowerCase();
|
|
123
|
-
|
|
130
|
+
const indexMatches = results.filter((n) => this.noteIndexEntryMatchesSearch(n, term));
|
|
131
|
+
const indexMatchedIds = new Set(indexMatches.map((n) => n.id));
|
|
132
|
+
const contentMatches = [];
|
|
133
|
+
const candidates = results.filter((n) => !indexMatchedIds.has(n.id));
|
|
134
|
+
for (const candidate of candidates) {
|
|
135
|
+
const note = await this.getNote(candidate.id);
|
|
136
|
+
if (!note) continue;
|
|
137
|
+
if ([
|
|
138
|
+
note.title,
|
|
139
|
+
notePlainText(note),
|
|
140
|
+
note.attachments?.map((a) => a.transcript).join(" ")
|
|
141
|
+
].filter(Boolean).join(" ").toLowerCase().includes(term)) contentMatches.push(candidate);
|
|
142
|
+
}
|
|
143
|
+
results = [...indexMatches, ...contentMatches];
|
|
124
144
|
}
|
|
125
145
|
const sortField = query.sortBy || "createdAt";
|
|
126
146
|
const sortDir = query.sortOrder === "asc" ? 1 : -1;
|
|
127
|
-
results = [...results].sort((a, b) =>
|
|
147
|
+
results = [...results].sort((a, b) => {
|
|
148
|
+
return ((a[sortField] ?? 0) - (b[sortField] ?? 0)) * sortDir;
|
|
149
|
+
});
|
|
128
150
|
const total = results.length;
|
|
129
151
|
const offset = query.offset || 0;
|
|
130
152
|
const limit = Math.min(query.limit || 50, 200);
|
|
153
|
+
const items = results.slice(offset, offset + limit);
|
|
131
154
|
return {
|
|
132
|
-
items
|
|
133
|
-
total
|
|
155
|
+
items,
|
|
156
|
+
total,
|
|
157
|
+
limit,
|
|
158
|
+
offset,
|
|
159
|
+
hasMore: offset + items.length < total
|
|
134
160
|
};
|
|
135
161
|
}
|
|
162
|
+
noteIndexEntryMatchesSearch(entry, term) {
|
|
163
|
+
return Boolean(entry.title?.toLowerCase().includes(term) || entry.snippet?.toLowerCase().includes(term) || entry.tags?.some((tag) => tag.toLowerCase().includes(term)) || entry.attachmentNames?.some((name) => name.toLowerCase().includes(term)));
|
|
164
|
+
}
|
|
136
165
|
async saveAttachment(noteId, fileName, buffer) {
|
|
137
166
|
const mediaDir = resolveNoteMediaDir(noteId);
|
|
138
167
|
await mkdir(mediaDir, { recursive: true });
|