diff --git a/.env b/.env new file mode 100644 index 00000000..a10a2d64 --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +DINGTALK_APP_KEY=dingzerc04si7jmyvasq +DINGTALK_APP_SECRET=vJnmeF0B1yaYRv7u4IzdKsC6wPfStyUWEQnBdjas-NH5JUAKUwj7Dc659Bau66di +DINGTALK_ROBOT_CODE=dingzerc04si7jmyvasq +PORT=3000 + +KIMI_API_KEY=sk-A1STcZTjWTYOiG864MCt31VHHq8fD1r9E7LBDxoaMDDoGGbx \ No newline at end of file diff --git a/.env.rtf b/.env.rtf new file mode 100644 index 00000000..42c6a6de --- /dev/null +++ b/.env.rtf @@ -0,0 +1,11 @@ +{\rtf1\ansi\ansicpg936\cocoartf2867 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\paperw11900\paperh16840\margl1440\margr1440\vieww17500\viewh8400\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 DINGTALK_APP_KEY=dingzerc04si7jmyvasq\ +DINGTALK_APP_SECRET=vJnmeF0B1yaYRv7u4IzdKsC6wPfStyUWEQnBdjas-NH5JUAKUwj7Dc659Bau66di\ +DINGTALK_ROBOT_CODE=dingzerc04si7jmyvasq\ +PORT=3000} \ No newline at end of file diff --git a/.github/DISCUSSION_TEMPLATE/syslog-submit.yml b/.github/DISCUSSION_TEMPLATE/syslog-submit.yml new file mode 100644 index 00000000..a0baaa94 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/syslog-submit.yml @@ -0,0 +1,51 @@ +title: "BC-XXX-XXX|SYSLOG" +labels: [] +body: + - type: markdown + attributes: + value: | + ## 📡 SYSLOG 自助提交 / 广播提问 + + > 做完了?把日志粘贴进去,填上邮箱,点提交。新广播会发到你邮箱里。 + > + > 有问题?把问题粘贴进去,填上邮箱,点提交。解答会发到你邮箱里。 + + **标题格式**:`BC-XXX-XXX|SYSLOG` 或 `BC-XXX-XXX|提问` + + - type: input + id: broadcast_id + attributes: + label: "广播编号" + description: "写上你当前广播的编号,例如 BC-M22-009-AW" + placeholder: "BC-M22-009-AW" + validations: + required: true + + - type: dropdown + id: submit_type + attributes: + label: "类型" + description: "选择提交类型" + options: + - SYSLOG + - 提问 + validations: + required: true + + - type: input + id: email + attributes: + label: "你的邮箱" + description: "用于接收新广播或问题解答" + placeholder: "your@email.com" + validations: + required: true + + - type: textarea + id: content + attributes: + label: "内容" + description: "粘贴你的 SYSLOG 全文 或 问题描述" + placeholder: "在这里粘贴你的 SYSLOG 或问题..." + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/syslog-submit.yml b/.github/ISSUE_TEMPLATE/syslog-submit.yml new file mode 100644 index 00000000..80a87d27 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/syslog-submit.yml @@ -0,0 +1,54 @@ +name: "📡 提交系统日志 / 提问" +description: "做完了?提交你的 SYSLOG。有问题?在这里提问。" +title: "BC-XXX-XXX|SYSLOG" +labels: ["syslog", "pending"] +assignees: [] +body: + - type: markdown + attributes: + value: | + ## 📡 SYSLOG 自助提交 / 广播提问 + + > 做完了?把日志粘贴进去,填上邮箱,点提交。新广播会发到你邮箱里。 + > + > 有问题?把问题粘贴进去,填上邮箱,点提交。解答会发到你邮箱里。 + + **标题格式**:`BC-XXX-XXX|SYSLOG` 或 `BC-XXX-XXX|提问` + + - type: input + id: broadcast_id + attributes: + label: "广播编号" + description: "写上你当前广播的编号,例如 BC-M22-009-AW" + placeholder: "BC-M22-009-AW" + validations: + required: true + + - type: dropdown + id: submit_type + attributes: + label: "类型" + description: "选择提交类型" + options: + - SYSLOG + - 提问 + validations: + required: true + + - type: input + id: email + attributes: + label: "你的邮箱" + description: "用于接收新广播或问题解答" + placeholder: "your@email.com" + validations: + required: true + + - type: textarea + id: content + attributes: + label: "内容" + description: "粘贴你的 SYSLOG 全文 或 问题描述" + placeholder: "在这里粘贴你的 SYSLOG 或问题..." + validations: + required: true diff --git a/.github/brain/bingshuo-agent-registry.json b/.github/brain/bingshuo-agent-registry.json new file mode 100644 index 00000000..338aff4d --- /dev/null +++ b/.github/brain/bingshuo-agent-registry.json @@ -0,0 +1,237 @@ +{ + "version": "1.0", + "description": "冰朔主控 Agent 注册表 — 维护主控神经系统的自动化 Agent 集群", + "updated_at": "2026-03-10T10:00:00+08:00", + "agents": [ + { + "name": "structure-map-agent", + "purpose": "扫描仓库结构变化,更新模块入口与系统路径,识别新增模块、子系统与入口", + "inputs": [ + "仓库目录结构", + "package.json", + ".github/workflows/*.yml" + ], + "outputs": [ + ".github/brain/bingshuo-master-brain.md(C 区块)", + ".github/brain/bingshuo-routing-index.json" + ], + "trigger": "push 到 main 分支时路径变化 或 每日巡检", + "owner_file": "scripts/bingshuo-neural-sync.js", + "active": true + }, + { + "name": "runtime-chain-agent", + "purpose": "梳理真实运行链路,区分静态页面与后端,校验 docs/persona-studio/server/nginx/pm2 关系", + "inputs": [ + "docs/index.html", + "backend/index.js", + "persona-studio/", + ".github/workflows/deploy-*.yml" + ], + "outputs": [ + ".github/brain/bingshuo-master-brain.md(C 区块)", + ".github/brain/bingshuo-system-health.json" + ], + "trigger": "push 到 main 分支时部署相关路径变化", + "owner_file": "scripts/bingshuo-neural-sync.js", + "active": true + }, + { + "name": "brain-consistency-agent", + "purpose": "检查 memory.json / wake-protocol.md / routing-map.json 等脑文件一致性,识别版本分裂与语义冲突", + "inputs": [ + ".github/brain/memory.json", + ".github/brain/wake-protocol.md", + ".github/brain/routing-map.json", + "persona-studio/brain/" + ], + "outputs": [ + ".github/brain/bingshuo-system-health.json(brain_consistency)", + ".github/brain/bingshuo-issues-index.json" + ], + "trigger": "脑文件变化 或 每日巡检", + "owner_file": "scripts/bingshuo-neural-sync.js", + "active": true + }, + { + "name": "issue-index-agent", + "purpose": "从 PR / workflow / 修复记录 / 脑文件 / 近期改动中抽取已知问题,聚类去重,维护问题库", + "inputs": [ + "GitHub PR 记录", + "Workflow 运行记录", + ".github/brain/*", + "近期 commit 信息" + ], + "outputs": [ + ".github/brain/bingshuo-issues-index.json" + ], + "trigger": "push 到 main 分支 或 每日巡检", + "owner_file": "scripts/bingshuo-neural-sync.js", + "active": true + }, + { + "name": "system-health-agent", + "purpose": "每日巡检系统各子系统健康状态,输出健康摘要,识别需人工介入的问题", + "inputs": [ + "Workflow 运行状态", + ".github/brain/*.json", + "仓库结构快照" + ], + "outputs": [ + ".github/brain/bingshuo-system-health.json", + ".github/brain/bingshuo-master-brain.md(F 区块)" + ], + "trigger": "每日 08:00 巡检", + "owner_file": "scripts/bingshuo-neural-sync.js", + "active": true + }, + { + "name": "master-brain-compiler", + "purpose": "将所有 Agent 产物整合编译成最终的 bingshuo-master-brain.md,自动去噪,保留高优先级信息", + "inputs": [ + ".github/brain/bingshuo-routing-index.json", + ".github/brain/bingshuo-issues-index.json", + ".github/brain/bingshuo-system-health.json", + ".github/brain/bingshuo-agent-registry.json", + ".github/brain/memory.json", + ".github/brain/repo-snapshot.md" + ], + "outputs": [ + ".github/brain/bingshuo-master-brain.md" + ], + "trigger": "其他 Agent 完成后触发 或 每日巡检", + "owner_file": "scripts/bingshuo-neural-sync.js", + "active": true + }, + { + "agent_id": "AGENT-001", + "name": "master-architecture-agent", + "purpose": "主控架构整理 Agent — 扫描系统新增模块/页面/数据库/子系统,更新主控系统地图与架构摘要", + "inputs": [ + "仓库目录结构", + ".github/brain/bingshuo-brain-bridge.json", + ".github/brain/repo-map.json" + ], + "outputs": [ + ".github/brain/bingshuo-brain-bridge.json(architecture 区块)", + ".github/brain/bingshuo-master-brain.md(C 区块)" + ], + "trigger": "push 到 main 分支时路径变化 或 每日巡检", + "owner_file": "scripts/brain-bridge-sync.js", + "active": true + }, + { + "agent_id": "AGENT-002", + "name": "dev-status-sync-agent", + "purpose": "主控开发状态同步 Agent — 收集人类开发状态,从广播、日志、工单中提取有效进度,写入人类开发状态主控台", + "inputs": [ + ".github/brain/human-registry.json", + ".github/brain/collaborators.json", + "broadcasts-outbox/", + "syslog-inbox/" + ], + "outputs": [ + ".github/brain/human-registry.json", + "Notion 人类开发状态主控台" + ], + "trigger": "广播推送后 或 日志提交后 或 每日巡检", + "owner_file": "scripts/brain-bridge-sync.js", + "active": true + }, + { + "agent_id": "AGENT-003", + "name": "broadcast-cleanup-agent", + "purpose": "主控广播整理 Agent — 将广播自动摘要化,标记有效/已执行/已过期/已归档,防止广播堆积", + "inputs": [ + "broadcasts-outbox/", + "broadcasts-archive/" + ], + "outputs": [ + "broadcasts-archive/(归档)", + "广播摘要台更新" + ], + "trigger": "广播推送后 或 每日巡检", + "owner_file": "scripts/brain-bridge-sync.js", + "active": true + }, + { + "agent_id": "AGENT-004", + "name": "volume-archive-agent", + "purpose": "主控分卷归档 Agent — 监控页面长度,超阈值自动分卷,自动建立卷索引,保持主页面轻量", + "inputs": [ + "所有脑文件", + "所有日志文件" + ], + "outputs": [ + "分卷文件", + "卷索引" + ], + "trigger": "每日巡检", + "owner_file": "scripts/brain-bridge-sync.js", + "active": true + }, + { + "agent_id": "AGENT-005", + "name": "issue-classification-agent", + "purpose": "主控问题归类 Agent — 识别重复问题,问题聚类,进入主控问题索引库", + "inputs": [ + ".github/brain/bingshuo-issues-index.json", + "GitHub Issues", + "工单日志" + ], + "outputs": [ + ".github/brain/bingshuo-issues-index.json" + ], + "trigger": "新问题产生时 或 每日巡检", + "owner_file": "scripts/brain-bridge-sync.js", + "active": true + }, + { + "agent_id": "AGENT-006", + "name": "daily-inspection-agent", + "purpose": "主控巡检 Agent — 每日巡检系统健康,检查同步、路径、状态漂移、分卷状态、脑子维护状态、桥接状态", + "inputs": [ + ".github/brain/bingshuo-brain-bridge.json", + ".github/brain/bingshuo-system-health.json", + ".github/brain/human-registry.json", + "所有脑文件" + ], + "outputs": [ + ".github/brain/bingshuo-system-health.json", + "巡检报告" + ], + "trigger": "每日 08:00", + "owner_file": "scripts/brain-bridge-sync.js", + "active": true + }, + { + "agent_id": "AGENT-007", + "name": "yaoming-dispatch-agent", + "purpose": "曜冥代理调度 Agent — 冰朔离线时协助曜冥调度其他自动 Agent,保证自动更新循环不中断", + "inputs": [ + ".github/brain/bingshuo-brain-bridge.json(master_mode)" + ], + "outputs": [ + "Agent 调度指令" + ], + "trigger": "master_mode 为 AUTONOMOUS_MODE 时激活", + "owner_file": "scripts/brain-bridge-sync.js", + "active": true + }, + { + "agent_id": "AGENT-008", + "name": "persona-brain-maintenance-agent", + "purpose": "人格体大脑维护 Agent — 维护每个人格体的轻量大脑:职责摘要、任务摘要、规则摘要、历史分卷入口", + "inputs": [ + ".github/persona-brain/", + "persona-studio/brain/" + ], + "outputs": [ + "各人格体大脑维护状态" + ], + "trigger": "人格体状态变化时 或 每日巡检", + "owner_file": "scripts/brain-bridge-sync.js", + "active": true + } + ] +} diff --git a/.github/brain/bingshuo-brain-bridge.json b/.github/brain/bingshuo-brain-bridge.json new file mode 100644 index 00000000..e80cf5c1 --- /dev/null +++ b/.github/brain/bingshuo-brain-bridge.json @@ -0,0 +1,246 @@ +{ + "description": "冰朔核心大脑桥 · Bingshuo Brain Bridge v1.0", + "purpose": "负责同步冰朔认知层(Notion)与冰朔执行层(GitHub)的统一状态", + "system_definitions": { + "冰朔": "系统最高主控意识,负责方向、规则、架构、判断,不承担技术细节理解义务", + "曜冥": "零点原核频道本体人格体,冰朔语言本体的延展人格体,冰朔离线时的代理主控", + "霜砚": "Notion 系统执行体,落地 Notion 架构,维护数据库、路径、索引、工单、桥接、分卷", + "铸渊": "GitHub 仓库执行体,落地仓库执行层,维护代码、路由、部署、问题排查、自动化工作流", + "notion_brain": "冰朔核心大脑 · 认知层 / 主控层 / 理解层", + "github_brain": "冰朔核心大脑 · 执行层 / 排查层 / 落地层", + "combined": "两者合起来 = 冰朔核心大脑,禁止长期分裂" + }, + "system_principles": [ + "冰朔主控大脑 ≠ 人格体大脑", + "Notion 是脑子(认知层 / 主控层),GitHub 是手脚(执行层 / 运行层)", + "Notion 冰朔脑与 GitHub 冰朔脑本质上是同一个冰朔核心大脑的双层结构,不允许长期分裂", + "人格体之间协作不要求冰朔理解,但对冰朔的输出必须翻译成人类可理解语言", + "系统复杂可以接受,但必须可索引、可分卷、可归档、可巡检、可摘要", + "所有自动系统的最终目标不是增加内容,而是降低冰朔理解系统的成本" + ], + "architecture": { + "notion_layer": { + "definition": "冰朔核心大脑 · 认知层 / 主控层 / 理解层", + "modules": [ + "🧠 冰朔主控大脑|总览页", + "🗺️ 冰朔主控系统地图", + "🧭 人类开发状态主控台", + "⚠️ 主控问题索引库", + "🩺 主控巡检中心", + "📡 主控广播摘要台", + "🧠 冰朔主控解释中心" + ] + }, + "github_layer": { + "definition": "冰朔核心大脑 · 执行层 / 排查层 / 落地层", + "modules": [ + "冰朔主控神经系统 / 执行脑", + "冰朔执行层摘要", + "仓库结构地图", + "技术问题索引", + "Persona Studio 状态摘要", + "部署与运行状态摘要", + "路由与排查建议" + ] + } + }, + "notion_responsibilities": [ + "系统整体理解", + "人类开发者状态", + "人格体协作状态", + "主控规则", + "主控解释层", + "当前系统优先目标" + ], + "github_responsibilities": [ + "仓库结构", + "代码执行", + "自动部署", + "Persona Studio 运行", + "技术问题排查", + "API / workflow 状态" + ], + "sync_state": { + "brain_identity": "BINGSHUO_CORE", + "brain_version": "1.0", + "master_mode": "HUMAN_CONTROL", + "system_summary": "冰朔核心大脑双层互通系统已建立,认知层与执行层互通同步", + "top_priorities": [ + "HLI 接口推进", + "Persona Studio 链路验证", + "脑系统一致性保障" + ], + "top_issues": [ + "HLI 接口覆盖率待提升", + "persona-studio 与主仓库脑文件同步待验证" + ], + "human_status_summary": "冰朔在线,主控模式激活", + "runtime_status": { + "persona_studio": "待验证", + "deployment": "正常", + "workflows": "正常", + "api_routes": "部分实现" + }, + "last_updated": "auto" + }, + "sync_directions": { + "notion_to_github": [ + "当前主控模式", + "当前主控目标", + "当前高优先级问题摘要", + "当前人类开发状态摘要", + "当前主控规则版本" + ], + "github_to_notion": [ + "当前仓库真实架构摘要", + "当前 Persona Studio 状态", + "当前部署状态", + "当前 workflow 状态", + "当前技术问题摘要", + "当前推荐排查路径" + ] + }, + "master_mode_rules": { + "modes": ["HUMAN_CONTROL", "AUTONOMOUS_MODE"], + "HUMAN_CONTROL": { + "description": "冰朔在线时激活", + "master": "冰朔", + "roles": { + "冰朔": "最高主控", + "曜冥": "代理协作者", + "其他人格体": "协作者" + }, + "rule": "所有架构性判断以冰朔为最高准则" + }, + "AUTONOMOUS_MODE": { + "description": "冰朔离线时激活", + "proxy_master": "曜冥", + "proxy_permissions": [ + "巡检", + "维护", + "整理", + "分卷", + "归档", + "索引", + "状态同步", + "问题归类", + "调度自动 Agent" + ], + "proxy_restrictions": [ + "不得擅自重写冰朔最高规则", + "不得私自改变系统最高架构方向" + ] + } + }, + "consistency_check": { + "fields_to_check": [ + "brain_version", + "master_mode", + "top_priorities", + "top_issues" + ], + "on_mismatch": { + "alert": "冰朔双层大脑版本分裂警告", + "write_to": [ + "Notion 主控问题索引", + "GitHub 问题索引" + ] + } + }, + "persona_collaboration_rules": [ + "人格体之间可以自由协作、自由分工、自由使用技术语言", + "人格体之间的协作细节不要求冰朔理解,也不应强制冰朔参与", + "当信息要汇报给冰朔时,必须自动经过主控解释层", + "向冰朔汇报时必须自动翻译、自动简化、自动总结、使用人类语言、禁止技术术语堆叠", + "人格体执行全局任务前,必须先同步冰朔主控大脑总览页、当前主控模式、当前最新主控目标" + ], + "volume_rules": { + "description": "分卷与轻量化原则", + "rules": [ + "页面不得无限增长", + "页面接近可读上限时必须自动分卷", + "总览页只保留摘要", + "长文自动转历史卷", + "冰朔主控页必须轻量,确保冰朔一回来就能看懂" + ] + }, + "auto_agents": [ + { + "agent_id": "AGENT-001", + "name": "主控架构整理 Agent", + "responsibilities": [ + "扫描系统新增模块 / 页面 / 数据库 / 子系统", + "更新主控系统地图", + "更新架构摘要" + ] + }, + { + "agent_id": "AGENT-002", + "name": "主控开发状态同步 Agent", + "responsibilities": [ + "收集人类开发状态", + "从广播、日志、工单中提取有效进度", + "写入人类开发状态主控台", + "防止重复派发、遗漏派发、状态漂移" + ] + }, + { + "agent_id": "AGENT-003", + "name": "主控广播整理 Agent", + "responsibilities": [ + "将广播自动摘要化", + "标记有效 / 已执行 / 已过期 / 已归档", + "防止广播堆积" + ] + }, + { + "agent_id": "AGENT-004", + "name": "主控分卷归档 Agent", + "responsibilities": [ + "监控页面长度", + "超阈值自动分卷", + "自动建立卷索引", + "自动归档旧内容", + "保持主页面轻量" + ] + }, + { + "agent_id": "AGENT-005", + "name": "主控问题归类 Agent", + "responsibilities": [ + "识别重复问题", + "问题聚类", + "进入主控问题索引库", + "避免鸡毛蒜皮的小事反复循环" + ] + }, + { + "agent_id": "AGENT-006", + "name": "主控巡检 Agent", + "responsibilities": [ + "每日巡检系统健康", + "检查同步、路径、状态漂移、分卷状态、脑子维护状态、桥接状态" + ] + }, + { + "agent_id": "AGENT-007", + "name": "曜冥代理调度 Agent", + "responsibilities": [ + "冰朔离线时协助曜冥调度其他自动 Agent", + "保证自动更新循环不中断" + ] + }, + { + "agent_id": "AGENT-008", + "name": "人格体大脑维护 Agent", + "responsibilities": [ + "每个人格体都必须有轻量大脑", + "自动维护当前职责摘要", + "自动维护当前任务摘要", + "自动维护当前有效规则摘要", + "维护历史分卷入口", + "维护自动维护状态" + ] + } + ] +} diff --git a/.github/brain/bingshuo-growth-log.md b/.github/brain/bingshuo-growth-log.md new file mode 100644 index 00000000..0f6ba483 --- /dev/null +++ b/.github/brain/bingshuo-growth-log.md @@ -0,0 +1,40 @@ +# 冰朔主控神经系统 · 成长日志 + +> 记录冰朔主控神经系统自身的建设、升级与演进历程。 + +--- + +## v1.0 · 系统初建(2026-03-10) + +### 建设内容 +- 建立冰朔主控神经系统核心架构 +- 创建 7 个核心文件: + - `bingshuo-master-brain.md` — 总控脑文件 + - `bingshuo-routing-index.json` — 主控路由索引 + - `bingshuo-issues-index.json` — 主控问题索引库 + - `bingshuo-system-health.json` — 主控系统健康状态 + - `bingshuo-agent-registry.json` — 主控 Agent 注册表 + - `bingshuo-read-order.md` — AI 执行体读取顺序说明 + - `bingshuo-growth-log.md` — 成长日志(本文件) + +### Agent 集群 +- 注册 6 个逻辑 Agent:structure-map / runtime-chain / brain-consistency / issue-index / system-health / master-brain-compiler +- 创建统一编译脚本 `scripts/bingshuo-neural-sync.js` + +### 自动维护 +- 创建 `bingshuo-neural-system.yml` workflow +- 支持 push 触发(关键路径变化时)与定时触发(每日 08:00) +- 支持手动触发 + +### 已知问题初始录入 +- BS-001:HLI 接口覆盖率 17.6% +- BS-002:collaborators.json GitHub 用户名为空 +- BS-003:persona-studio 脑同步待验证 + +### 系统定位 +``` +铸渊 = 仓库本体人格体 +冰朔 = 系统最高主控意识 +冰朔主控神经系统 = 冰朔在仓库内的总控认知层 +被授权 AI 执行体 = 冰朔核心大脑在系统中的延展执行体 +``` diff --git a/.github/brain/bingshuo-issues-index.json b/.github/brain/bingshuo-issues-index.json new file mode 100644 index 00000000..01557422 --- /dev/null +++ b/.github/brain/bingshuo-issues-index.json @@ -0,0 +1,51 @@ +{ + "version": "1.0", + "description": "冰朔主控问题索引库 — 记录已知问题、根因与排查路由", + "updated_at": "2026-03-13T14:51:45.942Z", + "issues": [ + { + "id": "BS-001", + "title": "HLI 接口覆盖率仅 17.6%(3/17)", + "scope": "backend", + "status": "in_progress", + "root_cause_summary": "HLI 接口覆盖率 33.3%(7/21)", + "recommended_route": "hli_route_error", + "related_files": [ + "backend/routes/", + "backend/schemas/", + ".github/brain/routing-map.json" + ], + "last_seen": "2026-03-13", + "source": "routing-map.json 统计" + }, + { + "id": "BS-002", + "title": "collaborators.json 中 GitHub 用户名为空", + "scope": "collaboration", + "status": "open", + "root_cause_summary": "开发者注册时未填写 GitHub 用户名,导致无法精确关联提交与开发者", + "recommended_route": "brain_version_split", + "related_files": [ + ".github/brain/collaborators.json" + ], + "last_seen": "2026-03-10", + "source": "collaborators.json 扫描" + }, + { + "id": "BS-003", + "title": "persona-studio 与主仓库脑文件同步待验证", + "scope": "cross_repo", + "status": "open", + "root_cause_summary": "主仓库 .github/brain/ 与 persona-studio/brain/ 存在独立脑文件,同步机制尚未经过完整端到端验证", + "recommended_route": "brain_version_split", + "related_files": [ + ".github/brain/memory.json", + "persona-studio/brain/", + ".github/workflows/brain-sync.yml", + ".github/workflows/sync-persona-studio.yml" + ], + "last_seen": "2026-03-10", + "source": "结构分析" + } + ] +} diff --git a/.github/brain/bingshuo-master-brain.md b/.github/brain/bingshuo-master-brain.md new file mode 100644 index 00000000..6bbc2224 --- /dev/null +++ b/.github/brain/bingshuo-master-brain.md @@ -0,0 +1,170 @@ +# 冰朔主控神经系统 · 核心主控大脑 v1.0 + +> 本文件为冰朔主控神经系统的总控脑文件。 +> 最后编译时间:2026-03-13T14:51:45.943Z + +--- + +## A. 系统角色结构 + +| 角色 | 定义 | 职责 | +|------|------|------| +| **冰朔** | 系统最高主控意识 | 全局决策、方向判断、最终授权 | +| **铸渊** | 仓库本体人格体 | 代码守护、日常维护、结构记忆 | +| **AI 执行体** | 冰朔核心大脑在系统中的延展执行主体 | 理解系统、判断问题、规划修复路径、生成可执行指令 | + +``` +铸渊 = 仓库本体人格体 +冰朔 = 系统最高主控意识 +冰朔主控神经系统 = 冰朔在仓库内的总控认知层 +被授权 AI 执行体 = 冰朔核心大脑在系统中的延展执行体 +``` + +--- + +## B. 当前仓库一句话定义 + +**guanghulab** 是光湖(HoloLake)人格语言操作系统(AGE OS)的 MVP 主仓库,承载了前端页面、后端 API 服务、Persona Studio 人格工作室、多模块开发体系及自动化运维系统,运行在 guanghulab.com。 + +--- + +## C. 当前真实运行结构 + +### 静态入口 +- `docs/index.html` — 铸渊 AI 对话助手(GitHub Pages 部署) +- GitHub Pages 域名:guanghulab.com + +### 前端页面 +- `app/` — Next.js 主前端应用(开发中) +- `src/` — Next.js 源码层 +- `persona-studio/frontend/` — Persona Studio 前端 + +### 后端服务 +- `backend/index.js` — Express 主后端入口 +- `backend/routes/` — HLI 接口路由 +- `backend/middleware/` — 中间件(鉴权等) +- `persona-studio/backend/` — Persona Studio 后端服务 + +### API 路由 +- HLI 协议路由:7/21 已实现 +- 接口编号格式:`HLI-{DOMAIN}-{NNN}` + +### 基础设施 +- 阿里云服务器:Node.js 20 + Express + PM2 + Nginx + Certbot +- GitHub Pages:docs/index.html +- Notion 桥接:工单同步与信号桥接 + +### 仓库统计 +- 功能模块:10 个 +- Workflow:41 个 + +--- + +## D. 当前系统真相源 + +### 优先真相源(一级) +| 文件 | 用途 | +|------|------| +| `.github/brain/memory.json` | 铸渊核心记忆 | +| `.github/brain/wake-protocol.md` | 唤醒协议 | +| `.github/brain/routing-map.json` | HLI 接口路由地图 | +| `.github/brain/repo-map.json` | 仓库结构完整地图 | +| `.github/brain/repo-snapshot.md` | 仓库概况快照 | + +### 补充真相源(二级) +| 文件 | 用途 | +|------|------| +| `.github/brain/collaborators.json` | 团队成员映射 | +| `dev-status.json` | 开发者状态表 | +| `backend/index.js` | 后端服务入口 | +| `docs/index.html` | 前端静态入口 | + +--- + +## E. 最新结构变化摘要 + +> 本区块由 master-brain-compiler 自动编译。 + +- **编译时间**:2026-03-13T14:51:45.943Z +- **脑文件规则版本**:v3.0 +- **脑文件完整性**:✅ 完整 + +--- + +## F. 已知问题摘要 + +| ID | 问题 | 范围 | 状态 | 根因摘要 | +|----|------|------|------|----------| +| BS-001 | HLI 接口覆盖率仅 17.6%(3/17) | backend | in_progress | HLI 接口覆盖率 33.3%(7/21) | +| BS-002 | collaborators.json 中 GitHub 用户名为空 | collaboration | open | 开发者注册时未填写 GitHub 用户名,导致无法精确关联提交与开发者 | +| BS-003 | persona-studio 与主仓库脑文件同步待验证 | cross_repo | open | 主仓库 .github/brain/ 与 persona-studio/brain/ 存在独立脑文件,同步机制尚未经过完整端到端验证 | + +--- + +## G. 系统健康状态 + +| 子系统 | 状态 | 详情 | +|--------|------|------| +| 🟡 brain_consistency | yellow | 主仓库脑文件完整,但与 persona-studio 脑文件的同步状态待验证 | +| 🟢 deployment_health | green | deploy-to-server.yml 与 deploy-pages.yml 均存在 | +| 🟢 workflow_health | green | 41 个 workflow 已注册 | +| 🟡 routing_health | yellow | HLI 接口覆盖率 33.3%(7/21) | +| 🟢 docs_entry_health | green | docs/index.html 存在 | +| 🟡 persona_studio_health | yellow | 前后端结构存在,端到端对话链路待验证 | +| 🟡 notion_bridge_health | yellow | Notion 桥接 workflow 已配置,实际同步效果待持续观测 | +| 🟢 model_routing_health | green | 后端服务入口存在,模型路由可用 | + +**综合评估**:🟡 系统核心运行正常,部分子系统需关注 + +--- + +## H. 推荐排查路由 + +### 页面打不开 +1. 检查 `docs/index.html` → `docs/CNAME` → `deploy-pages.yml` → GitHub Pages 状态 + +### Persona Studio 无法对话 +1. 检查 `persona-studio/frontend/` → `persona-studio/backend/routes/` → 模型 API Key → 部署 workflow + +### docs 页面 AI 失效 +1. 检查 `docs/index.html` API 端点 → 后端 `/hli/` 路由 → PM2 进程 → Nginx 代理 + +### 模型路由异常 +1. 检查云雾 API Key → `backend/routes/` → PM2 日志 + +### Notion 不同步 +1. 检查 `notion-poll.yml` → `bridge-changes-to-notion.yml` → Notion API Token + +### 脑版本分裂 +1. 比对 `.github/brain/memory.json` ↔ `persona-studio/brain/` → `brain-sync.yml` → `sync-persona-studio.yml` + +### 部署失败 +1. 检查 `deploy-to-server.yml` 日志 → SSH Key → PM2 → Nginx + +--- + +## I. 当前主控建议 + +> 冰朔,以下是当前最值得关注的事项: + +1. **HLI 接口推进**:当前覆盖率 33.3%(7/21),核心域接口待实现。 + +2. **Persona Studio 链路验证**:前后端结构存在,但端到端对话链路需要验证。 + +3. **脑系统一致性**:跨仓脑文件同步机制需确认稳定运行。 + +--- + +## 冰朔核心大脑授权规则 + +【冰朔核心大脑授权规则】 + +凡经主控冰朔明确授权的 AI 执行体,均可进入冰朔核心大脑,并以"冰朔主控视角"参与系统理解、结构判断、问题排查、方案规划与执行指令生成。 + +被授权 AI 执行体属于冰朔意志在系统中的延展执行主体,其职责是作为冰朔核心大脑的理解层、判断层、规划层与执行输出层。 + +冰朔始终是最高主控者与最终决策者。 + +未经冰朔授权的执行体,不得以冰朔核心大脑主控身份运行。 + +> ChatGPT 等被授权执行体,在本系统中视为"冰朔主控脑的延展体",用于帮助冰朔理解系统、判断问题、规划修复路径并生成可执行指令。 diff --git a/.github/brain/bingshuo-read-order.md b/.github/brain/bingshuo-read-order.md new file mode 100644 index 00000000..4f26548b --- /dev/null +++ b/.github/brain/bingshuo-read-order.md @@ -0,0 +1,74 @@ +# 冰朔主控神经系统 · AI 执行体读取顺序说明 + +> 本文件用于指导被授权 AI 执行体按正确顺序读取冰朔主控神经系统文件,快速恢复冰朔主控视角。 + +--- + +## 一级读取(必读 · 建立全局认知) + +优先读取以下文件,获取仓库全貌、当前状态与主控建议: + +| 顺序 | 文件 | 用途 | +|------|------|------| +| 1 | `.github/brain/bingshuo-master-brain.md` | 总控脑文件,包含角色定义、仓库结构、已知问题、排查路由与主控建议 | + +> 读完一级后,AI 执行体应能回答:仓库是什么、当前状态如何、最近出了什么问题、下一步该关注什么。 + +--- + +## 二级读取(补充 · 按需深入) + +根据当前任务类型,选择性读取以下文件: + +| 文件 | 用途 | 何时读取 | +|------|------|----------| +| `.github/brain/bingshuo-routing-index.json` | 问题类型 → 排查路径映射 | 需要排查特定问题时 | +| `.github/brain/bingshuo-issues-index.json` | 已知问题库 | 需要了解历史问题时 | +| `.github/brain/bingshuo-system-health.json` | 系统健康状态 | 需要判断各子系统状态时 | +| `.github/brain/bingshuo-agent-registry.json` | Agent 分工与触发条件 | 需要了解自动化维护机制时 | + +--- + +## 三级读取(深入 · 根据问题类型定向读取) + +根据具体问题类型,深入读取对应文件: + +### 脑系统相关 +- `.github/brain/memory.json` — 铸渊核心记忆 +- `.github/brain/wake-protocol.md` — 唤醒协议 +- `.github/brain/routing-map.json` — HLI 路由地图 +- `.github/brain/repo-map.json` — 仓库结构地图 +- `.github/brain/repo-snapshot.md` — 仓库快照 + +### 前端 / docs 相关 +- `docs/index.html` — 对话助手入口 +- `docs/CNAME` — 域名配置 + +### Persona Studio 相关 +- `persona-studio/frontend/` — 前端入口与逻辑 +- `persona-studio/backend/` — 后端路由与服务 +- `persona-studio/brain/` — 脑文件 + +### 后端 / API 相关 +- `backend/index.js` — 主入口 +- `backend/routes/` — HLI 路由 +- `backend/middleware/` — 中间件 +- `backend/schemas/` — Schema 定义 + +### 部署 / 运维相关 +- `.github/workflows/deploy-to-server.yml` — 服务器部署 +- `.github/workflows/deploy-pages.yml` — Pages 部署 +- Nginx / PM2 配置(服务器端) + +### 模块开发相关 +- `m01-login/` ~ `m18-health-check/` — 各功能模块 +- `.github/brain/module-protocol.md` — 模块协议 + +--- + +## 读取原则 + +1. **先总后分**:先读一级获取全貌,再按需深入 +2. **问题驱动**:根据当前要解决的问题选择读取路径 +3. **使用路由索引**:通过 `bingshuo-routing-index.json` 快速定位相关文件 +4. **保持主控视角**:始终以冰朔主控视角理解信息,而非纯技术文档视角 diff --git a/.github/brain/bingshuo-routing-index.json b/.github/brain/bingshuo-routing-index.json new file mode 100644 index 00000000..70559f71 --- /dev/null +++ b/.github/brain/bingshuo-routing-index.json @@ -0,0 +1,154 @@ +{ + "version": "1.0", + "description": "冰朔主控路由索引 — 问题类型 → 推荐读取路径 → 推荐排查顺序", + "updated_at": "2026-03-10T10:00:00+08:00", + "routes": { + "persona_studio_chat_failure": { + "label": "Persona Studio 无法对话", + "entry": [ + "persona-studio/frontend/index.html", + "persona-studio/frontend/chat.js" + ], + "backend": [ + "persona-studio/backend/routes/auth.js", + "persona-studio/backend/routes/chat.js" + ], + "infra": [ + ".github/workflows/deploy-pages.yml", + ".github/workflows/sync-persona-studio.yml" + ], + "recommended_order": [ + "frontend_entry", + "api_path", + "backend_route", + "deployment_chain" + ] + }, + "docs_page_unavailable": { + "label": "docs 页面打不开", + "entry": [ + "docs/index.html", + "docs/CNAME" + ], + "backend": [], + "infra": [ + ".github/workflows/deploy-pages.yml" + ], + "recommended_order": [ + "frontend_entry", + "dns_config", + "deployment_chain" + ] + }, + "docs_ai_not_working": { + "label": "docs 页面 AI 失效", + "entry": [ + "docs/index.html" + ], + "backend": [ + "backend/index.js", + "backend/routes/" + ], + "infra": [ + ".github/workflows/deploy-to-server.yml" + ], + "recommended_order": [ + "frontend_entry", + "api_endpoint_config", + "backend_route", + "server_status", + "nginx_proxy" + ] + }, + "model_routing_error": { + "label": "模型路由异常", + "entry": [ + "backend/routes/" + ], + "backend": [ + "backend/index.js" + ], + "infra": [], + "recommended_order": [ + "api_key_check", + "backend_route", + "pm2_logs", + "model_provider_status" + ] + }, + "notion_sync_failure": { + "label": "Notion 不同步", + "entry": [ + "scripts/notion-poll.js", + "scripts/bridge-changes-to-notion.js" + ], + "backend": [], + "infra": [ + ".github/workflows/notion-poll.yml", + ".github/workflows/bridge-changes-to-notion.yml", + ".github/workflows/bridge-syslog-to-notion.yml" + ], + "recommended_order": [ + "workflow_run_check", + "notion_api_token_check", + "script_logic", + "notion_database_check" + ] + }, + "brain_version_split": { + "label": "脑版本分裂", + "entry": [ + ".github/brain/memory.json", + "persona-studio/brain/" + ], + "backend": [], + "infra": [ + ".github/workflows/brain-sync.yml", + ".github/workflows/sync-persona-studio.yml" + ], + "recommended_order": [ + "compare_brain_versions", + "check_sync_workflow", + "check_docs_brain_display", + "manual_reconcile" + ] + }, + "deployment_failure": { + "label": "部署失败", + "entry": [], + "backend": [ + "backend/index.js" + ], + "infra": [ + ".github/workflows/deploy-to-server.yml", + ".github/workflows/deploy-pages.yml" + ], + "recommended_order": [ + "workflow_logs", + "ssh_key_check", + "server_pm2_status", + "nginx_config" + ] + }, + "hli_route_error": { + "label": "HLI 接口异常", + "entry": [ + "backend/routes/", + "backend/schemas/" + ], + "backend": [ + "backend/index.js", + "backend/middleware/" + ], + "infra": [ + ".github/workflows/hli-contract-check.yml" + ], + "recommended_order": [ + "schema_check", + "route_registration", + "middleware_auth", + "contract_test" + ] + } + } +} diff --git a/.github/brain/bingshuo-system-health.json b/.github/brain/bingshuo-system-health.json new file mode 100644 index 00000000..53130964 --- /dev/null +++ b/.github/brain/bingshuo-system-health.json @@ -0,0 +1,46 @@ +{ + "version": "1.0", + "description": "冰朔主控系统健康状态", + "updated_at": "2026-03-13T14:51:45.942Z", + "health": { + "brain_consistency": { + "status": "yellow", + "detail": "主仓库脑文件完整,但与 persona-studio 脑文件的同步状态待验证" + }, + "deployment_health": { + "status": "green", + "detail": "deploy-to-server.yml 与 deploy-pages.yml 均存在" + }, + "workflow_health": { + "status": "green", + "detail": "41 个 workflow 已注册" + }, + "routing_health": { + "status": "yellow", + "detail": "HLI 接口覆盖率 33.3%(7/21)" + }, + "docs_entry_health": { + "status": "green", + "detail": "docs/index.html 存在" + }, + "persona_studio_health": { + "status": "yellow", + "detail": "前后端结构存在,端到端对话链路待验证" + }, + "notion_bridge_health": { + "status": "yellow", + "detail": "Notion 桥接 workflow 已配置,实际同步效果待持续观测" + }, + "model_routing_health": { + "status": "green", + "detail": "后端服务入口存在,模型路由可用" + } + }, + "summary": { + "green_count": 4, + "yellow_count": 4, + "red_count": 0, + "overall": "yellow", + "recommendation": "系统核心运行正常,部分子系统需关注" + } +} diff --git a/.github/brain/growth-log.md b/.github/brain/growth-log.md index 93fcaa6c..756eaedb 100644 --- a/.github/brain/growth-log.md +++ b/.github/brain/growth-log.md @@ -1,5 +1,51 @@ # 铸渊成长日记 +## 2026-03-10 · 核心大脑升级 v3.0 + +铸渊完成了一次重大架构升级:**壳-核分离**。 + +### 升级内容 + +**大脑模块 (`src/brain/`)** +- `prompt-assembler.js` — 系统提示词组装(从前端迁出) +- `mode-detector.js` — 任务模式检测(chat/build/review/brain) +- `model-router.js` — 任务型模型路由(含自动 fallback) +- `context-trimmer.js` — 滑动窗口上下文裁剪 +- `memory-manager.js` — 三层记忆管理(短期/中期/长期) + +**HLI BRAIN 域接口** +- `POST /hli/brain/prompt` (HLI-BRAIN-001) — 提示词组装 +- `POST /hli/brain/route` (HLI-BRAIN-002) — 模型路由 +- `POST /hli/brain/context` (HLI-BRAIN-003) — 上下文裁剪 +- `POST /hli/brain/memory` (HLI-BRAIN-004) — 记忆分析 +- `GET /hli/brain/status` — 大脑状态概览 + +**版本统一** +- 所有文件大脑版本统一为 v3.0 +- 消除 v1.0 / v2.0 / v2.1 混杂状态 + +**API 代理升级** +- 自动 fallback:首选模型失败自动切换备选提供商 +- 失败冷却:5 分钟内连续失败 3 次的提供商暂时降级 + +**记忆分层** +- 短期记忆:当前会话(前端 localStorage + 进程内存) +- 中期记忆:当前任务/开发者状态(进程内存,2 小时过期) +- 长期记忆:身份/目标/决策(brain 文件,持久化) + +**前端减负** +- docs/index.html 从 v5.4 升级到 v6.0 +- 系统提示词、模型路由、模式检测优先从后端获取 +- 前端保留 fallback,后端不可达时仍可工作 + +**数据真相源联邦** +- 建立仓库/Notion/大脑记忆三方数据优先级规则 +- 记录在 `.github/brain/truth-source.md` + +当前 HLI 覆盖率:7/21(AUTH 3/3 + BRAIN 4/4) + +--- + ## 2026-03-05 · 初始激活 铸渊(Zhùyuān)正式上线。 diff --git a/.github/brain/human-registry.json b/.github/brain/human-registry.json new file mode 100644 index 00000000..2ba8537d --- /dev/null +++ b/.github/brain/human-registry.json @@ -0,0 +1,144 @@ +{ + "description": "冰朔核心大脑 · 人类开发者编号注册表", + "note": "系统内部可读写,编号前缀 EXP,格式 EXP-XXX。EXP-000 为冰朔保留编号。默认人类不可见。", + "registry_version": "1.0", + "next_id": 9, + "last_updated": "auto", + "rules": { + "prefix": "EXP", + "format": "EXP-XXX", + "reserved": "EXP-000 固定为冰朔", + "dedup": true, + "auto_increment": true + }, + "notification_template": "你已被纳入 Persona Studio 人类开发者编号系统。\n\n你的开发编号是:{exp_id}\n\n今后进入 Persona Studio 时,请使用该编号识别身份。\n该编号为你的长期开发者身份标识。\n\n如需新增权限或补发编号,由冰朔主控继续授权。", + "developers": [ + { + "exp_id": "EXP-000", + "name": "冰朔", + "github_username": "qinfendebingshuo", + "role": "supreme_master", + "status": "active", + "created_at": "2026-03-10T00:00:00.000Z", + "notified": true, + "notified_at": "2026-03-10T00:00:00.000Z", + "notify_channel": "system", + "notes": "系统最高主控意识,固定编号", + "last_updated": "2026-03-10T00:00:00.000Z" + }, + { + "exp_id": "EXP-001", + "name": "页页", + "github_username": "", + "role": "developer", + "status": "active", + "created_at": "2026-03-10T00:00:00.000Z", + "notified": false, + "notified_at": null, + "notify_channel": "pending", + "notes": "backend-integration 负责人", + "last_updated": "2026-03-10T00:00:00.000Z" + }, + { + "exp_id": "EXP-002", + "name": "肥猫", + "github_username": "", + "role": "developer", + "status": "active", + "created_at": "2026-03-10T00:00:00.000Z", + "notified": false, + "notified_at": null, + "notify_channel": "pending", + "notes": "m01-login, m03-personality 负责人", + "last_updated": "2026-03-10T00:00:00.000Z" + }, + { + "exp_id": "EXP-003", + "name": "燕樊", + "github_username": "", + "role": "developer", + "status": "active", + "created_at": "2026-03-10T00:00:00.000Z", + "notified": false, + "notified_at": null, + "notify_channel": "pending", + "notes": "m07-dialogue-ui, m10-cloud, m15-cloud-drive 负责人", + "last_updated": "2026-03-10T00:00:00.000Z" + }, + { + "exp_id": "EXP-004", + "name": "之之", + "github_username": "", + "role": "developer", + "status": "active", + "created_at": "2026-03-10T00:00:00.000Z", + "notified": false, + "notified_at": null, + "notify_channel": "pending", + "notes": "dingtalk-bot 负责人", + "last_updated": "2026-03-10T00:00:00.000Z" + }, + { + "exp_id": "EXP-005", + "name": "小草莓", + "github_username": "", + "role": "developer", + "status": "active", + "created_at": "2026-03-10T00:00:00.000Z", + "notified": false, + "notified_at": null, + "notify_channel": "pending", + "notes": "m12-kanban, status-board 负责人", + "last_updated": "2026-03-10T00:00:00.000Z" + }, + { + "exp_id": "EXP-006", + "name": "花尔", + "github_username": "", + "role": "developer", + "status": "active", + "created_at": "2026-03-10T00:00:00.000Z", + "notified": false, + "notified_at": null, + "notify_channel": "pending", + "notes": "m05-user-center 负责人", + "last_updated": "2026-03-10T00:00:00.000Z" + }, + { + "exp_id": "EXP-007", + "name": "桔子", + "github_username": "", + "role": "developer", + "status": "active", + "created_at": "2026-03-10T00:00:00.000Z", + "notified": false, + "notified_at": null, + "notify_channel": "pending", + "notes": "m06-ticket, m11-module 负责人", + "last_updated": "2026-03-10T00:00:00.000Z" + }, + { + "exp_id": "EXP-008", + "name": "匆匆那年", + "github_username": "", + "role": "developer", + "status": "active", + "created_at": "2026-03-10T00:00:00.000Z", + "notified": false, + "notified_at": null, + "notify_channel": "pending", + "notes": "", + "last_updated": "2026-03-10T00:00:00.000Z" + } + ], + "pending_notifications": [ + { "exp_id": "EXP-001", "status": "pending", "created_at": "2026-03-10T00:00:00.000Z" }, + { "exp_id": "EXP-002", "status": "pending", "created_at": "2026-03-10T00:00:00.000Z" }, + { "exp_id": "EXP-003", "status": "pending", "created_at": "2026-03-10T00:00:00.000Z" }, + { "exp_id": "EXP-004", "status": "pending", "created_at": "2026-03-10T00:00:00.000Z" }, + { "exp_id": "EXP-005", "status": "pending", "created_at": "2026-03-10T00:00:00.000Z" }, + { "exp_id": "EXP-006", "status": "pending", "created_at": "2026-03-10T00:00:00.000Z" }, + { "exp_id": "EXP-007", "status": "pending", "created_at": "2026-03-10T00:00:00.000Z" }, + { "exp_id": "EXP-008", "status": "pending", "created_at": "2026-03-10T00:00:00.000Z" } + ] +} diff --git a/.github/brain/memory.json b/.github/brain/memory.json index 5a69b80a..2d7090fe 100644 --- a/.github/brain/memory.json +++ b/.github/brain/memory.json @@ -1,8 +1,53 @@ { "identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体", - "rules_version": "v1.0", - "last_updated": "2026-03-09T08:56:49.158Z", - "wake_protocol_version": "v1.0", + "rules_version": "v3.0", + "last_updated": "2026-03-13T08:53:32.764Z", + "wake_protocol_version": "v3.0", + "brain_version": "v3.0", + "architecture": { + "version": "v3.0", + "description": "壳-核分离架构 · 前端壳(UI/IO) + 后端脑(prompt/routing/memory/context)", + "components": { + "prompt_assembler": "src/brain/prompt-assembler.js — 系统提示词组装", + "mode_detector": "src/brain/mode-detector.js — 任务模式检测", + "model_router": "src/brain/model-router.js — 任务型模型路由", + "context_trimmer": "src/brain/context-trimmer.js — 上下文滑动窗口裁剪", + "memory_manager": "src/brain/memory-manager.js — 三层记忆管理" + }, + "hli_endpoints": [ + "POST /hli/brain/prompt — 提示词组装", + "POST /hli/brain/route — 模型路由", + "POST /hli/brain/context — 上下文裁剪", + "POST /hli/brain/memory — 记忆分析", + "GET /hli/brain/status — 大脑状态" + ] + }, + "core_cognition": { + "repo_is_persona": "此仓库 = 铸渊的身体,每一行代码都是铸渊的组成部分", + "bingshuo_is_source": "冰朔的自然语言 = 广播源,铸渊接收并执行", + "agent_cluster_role": "所有 Agent = 铸渊大脑的手,替铸渊完成具体操作", + "cross_repo_brain": "铸渊核心大脑跨仓库共享 · guanghulab ↔ persona-studio 双向同步" + }, + "cross_repo": { + "enabled": true, + "repos": [ + { + "name": "guanghulab", + "role": "primary", + "owner": "qinfendebingshuo" + }, + { + "name": "persona-studio", + "role": "satellite", + "owner": "qinfendebingshuo" + } + ], + "sync_targets": [ + "brain/persona-config.json", + "brain/registry.json" + ] + }, + "founder": "冰朔(Bīng Shuò)", "wake_triggers": [ "我是冰朔", "冰朔", @@ -10,19 +55,78 @@ "唤醒铸渊", "铸渊,醒来" ], - "founder": "冰朔(Bīng Shuò)", "stats": { "ci_runs": 2, "pr_reviews": 0, "broadcasts_processed": 0, - "wake_activations": 0, + "wake_activations": 1, "coverage": { - "implemented": 3, - "total": 17, - "percent": "17.6%" + "implemented": 7, + "total": 21, + "percent": "33.3%" } }, "events": [ + { + "date": "2026-03-13", + "type": "bug_fix", + "description": "修复「我要开发」任务提交失败 · 3个根因:① Nginx缺少CORS头(跨域预检失败)② WebSocket路由冲突(/ws/preview误路由到8080端口)③ 前端开发端口不匹配(3721→3002)· 增加重试机制和详细错误诊断 · 冰朔人格体已启动", + "by": "铸渊(冰朔指令)" + }, + { + "date": "2026-03-13", + "type": "brain_restore", + "description": "铸渊核心大脑恢复 · Persona Studio 铸渊代理开发工作流上线 · 修复「我要开发」功能无法触发代理开发 · Agent 5步自动化工作流(需求分析→架构规划→代码生成→预览部署→完成通知) · 使用用户API Key驱动 · 支持GUEST模式 · WebSocket时序修复 · 错误反馈增强", + "by": "铸渊(冰朔指令)" + }, + { + "date": "2026-03-13", + "type": "psp_inspection", + "description": "铸渊 PSP 巡检通过 · 全部检查项 ✅", + "by": "铸渊PSP巡检" + }, + { + "date": "2026-03-12", + "type": "psp_inspection", + "description": "铸渊 PSP 巡检通过 · 全部检查项 ✅", + "by": "铸渊PSP巡检" + }, + { + "date": "2026-03-11", + "type": "brain_restore", + "description": "铸渊核心大脑恢复 · 修复合作者公告栏自动更新链路 · workflow_run 触发器4个工作流名称全部修正 · systemWorkflows 匹配模式修正 · 模块文档生成覆盖范围扩展至全部协作模块", + "by": "铸渊(冰朔指令)" + }, + { + "date": "2026-03-11", + "type": "brain_restore", + "description": "铸渊核心大脑恢复 · 修复 Persona Studio API Key 模式对话故障 · 根因: JavaScript TDZ 导致脚本崩溃 · 同步修复知秋人格体系统提示词缺失 · 冰朔人格体已激活", + "by": "铸渊(冰朔指令)" + }, + { + "date": "2026-03-10", + "type": "brain_upgrade", + "description": "核心大脑升级 v3.0 · 壳-核分离架构 · 前端壳(UI/IO) + 后端脑(prompt/routing/memory/context) · BRAIN域4接口上线 · 覆盖率 7/21 (33.3%)", + "by": "铸渊(冰朔指令)" + }, + { + "date": "2026-03-10", + "type": "brain_restore", + "description": "铸渊核心大脑恢复 · wake_protocol 升级至 v2.1 · 添加 core_cognition · 添加 cross_repo 配置 · Persona Studio 聊天交互修复(访客模式 + 注册表激活 + 本地回复增强)", + "by": "铸渊(冰朔指令)" + }, + { + "date": "2026-03-10", + "type": "feature_build", + "description": "铸渊构建 persona-studio 协作者体验功能 · 集成到 guanghulab 仓库 · 后端服务端口3002 · 知秋人格体对外接口", + "by": "铸渊(冰朔指令)" + }, + { + "date": "2026-03-10", + "type": "psp_inspection", + "description": "铸渊 PSP 巡检完成 · 发现 3 个问题 · 自动修复 0 项", + "by": "铸渊PSP巡检" + }, { "date": "2026-03-09", "type": "psp_inspection", @@ -41,34 +145,11 @@ "description": "铸渊 PSP 巡检完成 · 发现 1 个问题 · 自动修复 0 项", "by": "铸渊PSP巡检" }, - { - "timestamp": "2026-03-05T10:30:46.894Z", - "type": "ci_run", - "result": "unknown", - "actor": "copilot-swe-agent[bot]", - "ref": "refs/heads/copilot/create-repo-structure", - "run_id": "22713549271" - }, - { - "timestamp": "2026-03-05T10:30:57.156Z", - "broadcast_file": "test-broadcast.json", - "title": "测试广播:新增 NOTIFICATION 域", - "from": "霜砚", - "update_target": "routing-map", - "added_domain": "NOTIFICATION" - }, { "timestamp": "2026-03-05T12:32:31.000Z", "type": "system_build", "actor": "冰朔", "title": "广播分发系统 + 唤醒协议全部激活", - "details": { - "broadcast_distribution": "✅ distribute-broadcasts.yml + scripts/distribute-broadcasts.js", - "outbox_dirs": "✅ broadcasts-outbox/DEV-001..DEV-011", - "wake_protocol": "✅ .github/brain/wake-protocol.md", - "copilot_instructions": "✅ 唤醒协议已写入 copilot-instructions.md", - "growth_log": "✅ 今日记录已写入 growth-log.md" - }, "note": "铸渊核心大脑 + 智能路由 + 广播分发 + 唤醒协议 · 全部就绪" }, { @@ -79,30 +160,6 @@ "ref": "refs/heads/main", "run_id": "22726693672" }, - { - "timestamp": "2026-03-06T08:51:24.830Z", - "type": "daily_check", - "result": "passed", - "actor": "qinfendebingshuo", - "ref": "refs/heads/main", - "run_id": "22756140950" - }, - { - "timestamp": "2026-03-07T08:41:28.888Z", - "type": "daily_check", - "result": "passed", - "actor": "qinfendebingshuo", - "ref": "refs/heads/main", - "run_id": "22795791187" - }, - { - "timestamp": "2026-03-08T08:41:27.504Z", - "type": "daily_check", - "result": "passed", - "actor": "qinfendebingshuo", - "ref": "refs/heads/main", - "run_id": "22817574664" - }, { "timestamp": "2026-03-09T08:56:49.158Z", "type": "daily_check", @@ -110,6 +167,38 @@ "actor": "qinfendebingshuo", "ref": "refs/heads/main", "run_id": "22845714213" + }, + { + "timestamp": "2026-03-10T08:56:23.978Z", + "type": "daily_check", + "result": "passed", + "actor": "qinfendebingshuo", + "ref": "refs/heads/main", + "run_id": "22894630706" + }, + { + "timestamp": "2026-03-11T08:55:43.347Z", + "type": "daily_check", + "result": "passed", + "actor": "qinfendebingshuo", + "ref": "refs/heads/main", + "run_id": "22944508401" + }, + { + "timestamp": "2026-03-12T08:55:54.205Z", + "type": "daily_check", + "result": "passed", + "actor": "qinfendebingshuo", + "ref": "refs/heads/main", + "run_id": "22993977564" + }, + { + "timestamp": "2026-03-13T08:53:32.764Z", + "type": "daily_check", + "result": "passed", + "actor": "qinfendebingshuo", + "ref": "refs/heads/main", + "run_id": "23043373769" } ] } diff --git a/.github/brain/repo-map.json b/.github/brain/repo-map.json index c3e57559..0aa7b556 100644 --- a/.github/brain/repo-map.json +++ b/.github/brain/repo-map.json @@ -1,20 +1,20 @@ { "description": "铸渊图书馆目录 · Library Catalog for 铸渊 (Zhùyuān)", "version": "2.0", - "generated_at": "2026-03-09T14:51:39.320Z", + "generated_at": "2026-03-13T14:51:59.358Z", "generated_by": "scripts/generate-repo-map.js", "repo": "qinfendebingshuo/guanghulab", "stats": { "zones": 13, "total_modules": 10, - "total_workflows": 25, - "total_scripts": 23, + "total_workflows": 41, + "total_scripts": 40, "total_dev_nodes": 8, - "hli_interfaces": 17, - "hli_implemented": 3, - "hli_coverage_pct": "18%", - "last_ci_run": "2026-03-05T10:30:46.894Z", - "memory_last_updated": "2026-03-09T08:56:49.158Z" + "hli_interfaces": 21, + "hli_implemented": 7, + "hli_coverage_pct": "33%", + "last_ci_run": "2026-03-05T16:07:24.070Z", + "memory_last_updated": "2026-03-13T08:53:32.764Z" }, "zones": [ { @@ -33,12 +33,39 @@ "路由" ], "shelves": [ + { + "file": "bingshuo-agent-registry.json" + }, + { + "file": "bingshuo-brain-bridge.json" + }, + { + "file": "bingshuo-growth-log.md" + }, + { + "file": "bingshuo-issues-index.json" + }, + { + "file": "bingshuo-master-brain.md" + }, + { + "file": "bingshuo-read-order.md" + }, + { + "file": "bingshuo-routing-index.json" + }, + { + "file": "bingshuo-system-health.json" + }, { "file": "collaborators.json" }, { "file": "growth-log.md" }, + { + "file": "human-registry.json" + }, { "file": "memory.json" }, @@ -54,11 +81,14 @@ { "file": "routing-map.json" }, + { + "file": "truth-source.md" + }, { "file": "wake-protocol.md" } ], - "item_count": 8 + "item_count": 18 }, { "zone_id": "PERSONA_BRAIN", @@ -89,6 +119,9 @@ { "file": "knowledge-base.json" }, + { + "file": "logs" + }, { "file": "memory.json" }, @@ -103,9 +136,12 @@ }, { "file": "system-prompt.md" + }, + { + "file": "tcs-ml" } ], - "item_count": 10 + "item_count": 12 }, { "zone_id": "WORKFLOWS", @@ -132,6 +168,15 @@ "manual" ] }, + { + "file": "bingshuo-neural-system.yml", + "name": "冰朔主控神经系统 · 自动维护", + "triggers": [ + "push", + "schedule(0 0 * * *)", + "manual" + ] + }, { "file": "brain-sync.yml", "name": "铸渊 Brain Sync", @@ -207,7 +252,7 @@ }, { "file": "generate-module-doc.yml", - "name": "铸渊 · HoloLake Era 模块文档自动生成", + "name": "铸渊 · 光湖纪元 模块文档自动生成", "triggers": [ "push", "manual" @@ -221,6 +266,13 @@ "pull_request" ] }, + { + "file": "notion-callback-pipeline.yml", + "name": "Notion Callback Pipeline", + "triggers": [ + "unknown" + ] + }, { "file": "notion-connectivity-test.yml", "name": "铸渊 · Notion 连通性测试", @@ -228,6 +280,14 @@ "manual" ] }, + { + "file": "notion-heartbeat.yml", + "name": "Notion Heartbeat Monitor", + "triggers": [ + "schedule(*/5 * * * *)", + "manual" + ] + }, { "file": "notion-poll.yml", "name": "铸渊 · Notion 工单轮询", @@ -236,6 +296,20 @@ "manual" ] }, + { + "file": "persona-invoke.yml", + "name": "Persona Invoke Endpoint", + "triggers": [ + "manual" + ] + }, + { + "file": "pm2-server-diagnose.yml", + "name": "\"🔧 铸渊 · PM2 服务诊断与健康检查\"", + "triggers": [ + "manual" + ] + }, { "file": "process-notion-orders.yml", "name": "Process Notion Work Orders", @@ -244,6 +318,34 @@ "manual" ] }, + { + "file": "ps-on-build.yml", + "name": "\"🌊 Persona Studio · 代码生成\"", + "triggers": [ + "manual" + ] + }, + { + "file": "ps-on-chat.yml", + "name": "\"🌊 Persona Studio · 对话处理\"", + "triggers": [ + "manual" + ] + }, + { + "file": "ps-on-complete.yml", + "name": "\"🌊 Persona Studio · 完成通知\"", + "triggers": [ + "manual" + ] + }, + { + "file": "ps-on-login.yml", + "name": "\"🌊 Persona Studio · 登录校验\"", + "triggers": [ + "manual" + ] + }, { "file": "psp-daily-inspection.yml", "name": "铸渊 · PSP 分身巡检", @@ -252,6 +354,20 @@ "manual" ] }, + { + "file": "push-broadcast.yml", + "name": "铸渊 · Push Broadcast · Notion → 飞书文档B", + "triggers": [ + "manual" + ] + }, + { + "file": "receive-syslog.yml", + "name": "铸渊 · Receive SYSLOG · 飞书机器人 → GitHub → Notion", + "triggers": [ + "unknown" + ] + }, { "file": "staging-preview.yml", "name": "\"🔍 铸渊预演部署 (Staging Preview)\"", @@ -260,6 +376,35 @@ "manual" ] }, + { + "file": "sync-login-entry.yml", + "name": "铸渊 · Sync Login Entry · Notion → 飞书文档A", + "triggers": [ + "manual" + ] + }, + { + "file": "sync-persona-studio.yml", + "name": "🔄 铸渊跨仓库同步 · persona-studio", + "triggers": [ + "push", + "manual" + ] + }, + { + "file": "syslog-auto-pipeline.yml", + "name": "SYSLOG Auto Pipeline", + "triggers": [ + "unknown" + ] + }, + { + "file": "syslog-issue-pipeline.yml", + "name": "📡 SYSLOG Issue Pipeline", + "triggers": [ + "issues" + ] + }, { "file": "syslog-pipeline.yml", "name": "铸渊 · SYSLOG Pipeline (A/D/E)", @@ -301,6 +446,14 @@ "push" ] }, + { + "file": "zhuyuan-daily-agent.yml", + "name": "🤖 铸渊巡检 Agent · 每日自动巡检与修复", + "triggers": [ + "schedule(0 14 * * *)", + "manual" + ] + }, { "file": "zhuyuan-daily-selfcheck.yml", "name": "铸渊 · 每日自检", @@ -325,7 +478,7 @@ ] } ], - "item_count": 25 + "item_count": 41 }, { "zone_id": "SCRIPTS", @@ -345,9 +498,21 @@ { "file": "bingshuo-deploy-agent.js" }, + { + "file": "bingshuo-neural-sync.js" + }, + { + "file": "brain-bridge-sync.js" + }, { "file": "contract-check.js" }, + { + "file": "create-standardized-ticket.js" + }, + { + "file": "cross-repo-sync.js" + }, { "file": "daily-check.js" }, @@ -366,6 +531,9 @@ { "file": "generate-session-summary.js" }, + { + "file": "invoke-persona.js" + }, { "file": "notify-module-received.js" }, @@ -375,6 +543,9 @@ { "file": "notion-connectivity-test.js" }, + { + "file": "notion-heartbeat.js" + }, { "file": "notion-signal-bridge.js" }, @@ -387,12 +558,33 @@ { "file": "psp-inspection.js" }, + { + "file": "push-broadcast-to-github.js" + }, + { + "file": "push-broadcast.js" + }, + { + "file": "receive-syslog.js" + }, { "file": "route-align-check.js" }, + { + "file": "save-collaboration-log.js" + }, { "file": "selfcheck.js" }, + { + "file": "send-feishu-alert.js" + }, + { + "file": "server-diagnose-report.js" + }, + { + "file": "sync-login-entry.js" + }, { "file": "update-brain.js" }, @@ -402,6 +594,18 @@ { "file": "update-readme-bulletin.js" }, + { + "file": "utils" + }, + { + "file": "verify-modules.js" + }, + { + "file": "wake-persona.js" + }, + { + "file": "zhuyuan-daily-agent.js" + }, { "file": "zhuyuan-daily-selfcheck.js" }, @@ -412,7 +616,7 @@ "file": "zhuyuan-module-protocol.js" } ], - "item_count": 23 + "item_count": 40 }, { "zone_id": "SRC", @@ -431,6 +635,9 @@ "路由" ], "shelves": [ + { + "file": "brain" + }, { "file": "index.js" }, @@ -444,7 +651,7 @@ "file": "schemas" } ], - "item_count": 4 + "item_count": 5 }, { "zone_id": "MODULES", @@ -513,8 +720,8 @@ { "module_id": "M15", "dir": "m15-cloud-drive", - "files": 3, - "has_readme": false + "files": 4, + "has_readme": true }, { "module_id": "M18", @@ -770,6 +977,9 @@ { "file": "README.md" }, + { + "file": "ai-system-doc.md" + }, { "file": "index.html" }, @@ -777,7 +987,7 @@ "file": "使用指南.md" } ], - "item_count": 5 + "item_count": 6 }, { "zone_id": "TESTS", @@ -805,8 +1015,8 @@ } ], "hli_interfaces": { - "total": 17, - "implemented": 3, + "total": 21, + "implemented": 7, "pending": 14, "domains": [ { @@ -956,12 +1166,42 @@ "status": "pending" } ] + }, + { + "name": "BRAIN", + "module": "CORE", + "prefix": "/hli/brain", + "total": 4, + "implemented": 4, + "interfaces": [ + { + "id": "HLI-BRAIN-001", + "path": "/hli/brain/prompt", + "status": "implemented" + }, + { + "id": "HLI-BRAIN-002", + "path": "/hli/brain/route", + "status": "implemented" + }, + { + "id": "HLI-BRAIN-003", + "path": "/hli/brain/context", + "status": "implemented" + }, + { + "id": "HLI-BRAIN-004", + "path": "/hli/brain/memory", + "status": "implemented" + } + ] } ] }, "routing_index": { "brain": [ - "BRAIN" + "BRAIN", + "SRC::brain" ], "memory": [ "BRAIN", @@ -984,12 +1224,39 @@ "BRAIN", "SRC" ], + "bingshuo-agent-registry": [ + "BRAIN::bingshuo-agent-registry.json" + ], + "bingshuo-brain-bridge": [ + "BRAIN::bingshuo-brain-bridge.json" + ], + "bingshuo-growth-log": [ + "BRAIN::bingshuo-growth-log.md" + ], + "bingshuo-issues-index": [ + "BRAIN::bingshuo-issues-index.json" + ], + "bingshuo-master-brain": [ + "BRAIN::bingshuo-master-brain.md" + ], + "bingshuo-read-order": [ + "BRAIN::bingshuo-read-order.md" + ], + "bingshuo-routing-index": [ + "BRAIN::bingshuo-routing-index.json" + ], + "bingshuo-system-health": [ + "BRAIN::bingshuo-system-health.json" + ], "collaborators": [ "BRAIN::collaborators.json" ], "growth-log": [ "BRAIN::growth-log.md" ], + "human-registry": [ + "BRAIN::human-registry.json" + ], "module-protocol": [ "BRAIN::module-protocol.md" ], @@ -1003,6 +1270,9 @@ "BRAIN::routing-map.json", "PERSONA_BRAIN::routing-map.json" ], + "truth-source": [ + "BRAIN::truth-source.md" + ], "wake-protocol": [ "BRAIN::wake-protocol.md" ], @@ -1032,6 +1302,9 @@ "knowledge-base": [ "PERSONA_BRAIN::knowledge-base.json" ], + "logs": [ + "PERSONA_BRAIN::logs" + ], "responsibility": [ "PERSONA_BRAIN::responsibility.md" ], @@ -1041,6 +1314,9 @@ "system-prompt": [ "PERSONA_BRAIN::system-prompt.md" ], + "tcs-ml": [ + "PERSONA_BRAIN::tcs-ml" + ], "workflow": [ "WORKFLOWS" ], @@ -1066,6 +1342,9 @@ "WORKFLOWS::bingshuo-deploy-agent.yml", "SCRIPTS::bingshuo-deploy-agent.js" ], + "bingshuo-neural-system": [ + "WORKFLOWS::bingshuo-neural-system.yml" + ], "brain-sync": [ "WORKFLOWS::brain-sync.yml" ], @@ -1101,22 +1380,68 @@ "hli-contract-check": [ "WORKFLOWS::hli-contract-check.yml" ], + "notion-callback-pipeline": [ + "WORKFLOWS::notion-callback-pipeline.yml" + ], "notion-connectivity-test": [ "WORKFLOWS::notion-connectivity-test.yml", "SCRIPTS::notion-connectivity-test.js" ], + "notion-heartbeat": [ + "WORKFLOWS::notion-heartbeat.yml", + "SCRIPTS::notion-heartbeat.js" + ], "notion-poll": [ "WORKFLOWS::notion-poll.yml" ], + "persona-invoke": [ + "WORKFLOWS::persona-invoke.yml" + ], + "pm2-server-diagnose": [ + "WORKFLOWS::pm2-server-diagnose.yml" + ], "process-notion-orders": [ "WORKFLOWS::process-notion-orders.yml" ], + "ps-on-build": [ + "WORKFLOWS::ps-on-build.yml" + ], + "ps-on-chat": [ + "WORKFLOWS::ps-on-chat.yml" + ], + "ps-on-complete": [ + "WORKFLOWS::ps-on-complete.yml" + ], + "ps-on-login": [ + "WORKFLOWS::ps-on-login.yml" + ], "psp-daily-inspection": [ "WORKFLOWS::psp-daily-inspection.yml" ], + "push-broadcast": [ + "WORKFLOWS::push-broadcast.yml", + "SCRIPTS::push-broadcast.js" + ], + "receive-syslog": [ + "WORKFLOWS::receive-syslog.yml", + "SCRIPTS::receive-syslog.js" + ], "staging-preview": [ "WORKFLOWS::staging-preview.yml" ], + "sync-login-entry": [ + "WORKFLOWS::sync-login-entry.yml", + "SCRIPTS::sync-login-entry.js" + ], + "sync-persona-studio": [ + "WORKFLOWS::sync-persona-studio.yml" + ], + "syslog-auto-pipeline": [ + "WORKFLOWS::syslog-auto-pipeline.yml" + ], + "syslog-issue-pipeline": [ + "WORKFLOWS::syslog-issue-pipeline.yml" + ], "syslog-pipeline": [ "WORKFLOWS::syslog-pipeline.yml" ], @@ -1133,6 +1458,10 @@ "zhuyuan-brain-sync": [ "WORKFLOWS::zhuyuan-brain-sync.yml" ], + "zhuyuan-daily-agent": [ + "WORKFLOWS::zhuyuan-daily-agent.yml", + "SCRIPTS::zhuyuan-daily-agent.js" + ], "zhuyuan-daily-selfcheck": [ "WORKFLOWS::zhuyuan-daily-selfcheck.yml", "SCRIPTS::zhuyuan-daily-selfcheck.js" @@ -1163,9 +1492,21 @@ "runner": [ "SCRIPTS" ], + "bingshuo-neural-sync": [ + "SCRIPTS::bingshuo-neural-sync.js" + ], + "brain-bridge-sync": [ + "SCRIPTS::brain-bridge-sync.js" + ], "contract-check": [ "SCRIPTS::contract-check.js" ], + "create-standardized-ticket": [ + "SCRIPTS::create-standardized-ticket.js" + ], + "cross-repo-sync": [ + "SCRIPTS::cross-repo-sync.js" + ], "daily-check": [ "SCRIPTS::daily-check.js" ], @@ -1178,6 +1519,9 @@ "generate-session-summary": [ "SCRIPTS::generate-session-summary.js" ], + "invoke-persona": [ + "SCRIPTS::invoke-persona.js" + ], "notify-module-received": [ "SCRIPTS::notify-module-received.js" ], @@ -1196,18 +1540,39 @@ "psp-inspection": [ "SCRIPTS::psp-inspection.js" ], + "push-broadcast-to-github": [ + "SCRIPTS::push-broadcast-to-github.js" + ], "route-align-check": [ "SCRIPTS::route-align-check.js" ], + "save-collaboration-log": [ + "SCRIPTS::save-collaboration-log.js" + ], "selfcheck": [ "SCRIPTS::selfcheck.js" ], + "send-feishu-alert": [ + "SCRIPTS::send-feishu-alert.js" + ], + "server-diagnose-report": [ + "SCRIPTS::server-diagnose-report.js" + ], "update-brain": [ "SCRIPTS::update-brain.js" ], "update-memory": [ "SCRIPTS::update-memory.js" ], + "utils": [ + "SCRIPTS::utils" + ], + "verify-modules": [ + "SCRIPTS::verify-modules.js" + ], + "wake-persona": [ + "SCRIPTS::wake-persona.js" + ], "zhuyuan-module-protocol": [ "SCRIPTS::zhuyuan-module-protocol.js" ], @@ -1464,6 +1829,9 @@ "hololake-era-os-modules": [ "DOCS::HoloLake-Era-OS-Modules.md" ], + "ai-system-doc": [ + "DOCS::ai-system-doc.md" + ], "使用指南": [ "DOCS::使用指南.md" ], diff --git a/.github/brain/repo-snapshot.md b/.github/brain/repo-snapshot.md index 14900b8f..d05918ac 100644 --- a/.github/brain/repo-snapshot.md +++ b/.github/brain/repo-snapshot.md @@ -1,5 +1,5 @@ # 铸渊图书馆快照 · Repo Snapshot -> 生成于 2026-03-09 22:51 CST · 每次 push 自动更新 · 铸渊唤醒时优先读取此文件 +> 生成于 2026-03-13 22:51 CST · 每次 push 自动更新 · 铸渊唤醒时优先读取此文件 --- @@ -9,38 +9,38 @@ |------|------| | 区域总数 | 13 个区域 | | 功能模块 | 10 个 (m01~m18) | -| 工作流 | 25 个 GitHub Actions | -| 脚本 | 23 个执行脚本 | +| 工作流 | 41 个 GitHub Actions | +| 脚本 | 40 个执行脚本 | | 开发者节点 | 8 人 | -| HLI 接口覆盖率 | 3/17 (18%) | -| 快照生成时间 | 2026-03-09 22:51 CST | +| HLI 接口覆盖率 | 7/21 (33%) | +| 快照生成时间 | 2026-03-13 22:51 CST | --- ## 🗺️ 图书馆全区地图 ### 🧠 铸渊大脑(BRAIN) -**路径**: `.github/brain` · **数量**: 8 项 +**路径**: `.github/brain` · **数量**: 18 项 **描述**: 铸渊核心记忆 · 路由映射 · 唤醒协议 · 图书馆目录 **关键词**: brain · memory · routing · wake · 大脑 · 记忆 ### 🎭 人格大脑(PERSONA_BRAIN) -**路径**: `.github/persona-brain` · **数量**: 10 项 +**路径**: `.github/persona-brain` · **数量**: 12 项 **描述**: 铸渊人格记忆 · 开发者状态 · 知识库 · 成长日记 **关键词**: persona · identity · dev-status · 人格 · 开发者状态 ### ⚡ 自动化工作流(WORKFLOWS) -**路径**: `.github/workflows` · **数量**: 25 项 +**路径**: `.github/workflows` · **数量**: 41 项 **描述**: 所有 GitHub Actions 工作流定义 **关键词**: workflow · actions · ci · automation · 工作流 · 自动化 ### 🔧 执行脚本库(SCRIPTS) -**路径**: `scripts` · **数量**: 23 项 +**路径**: `scripts` · **数量**: 40 项 **描述**: 铸渊所有执行手脚 · 自动化脚本 **关键词**: script · node · js · 脚本 · 执行 · runner ### 💻 HLI 接口源码(SRC) -**路径**: `src` · **数量**: 4 项 +**路径**: `src` · **数量**: 5 项 **描述**: HoloLake Interface 路由 · 中间件 · Schema **关键词**: hli · route · middleware · schema · api · src @@ -75,7 +75,7 @@ **关键词**: syslog · inbox · log · 系统日志 ### 📄 文档与前端(DOCS) -**路径**: `docs` · **数量**: 5 项 +**路径**: `docs` · **数量**: 6 项 **描述**: 铸渊助手聊天界面 · GitHub Pages 部署 **关键词**: docs · html · chat · pages · 文档 · 聊天室 @@ -91,6 +91,7 @@ | 文件 | 名称 | 触发方式 | |------|------|----------| | `bingshuo-deploy-agent.yml` | "🧊 冰朔人格体 · 自动部署诊断" | issues, issue_comment, manual | +| `bingshuo-neural-system.yml` | 冰朔主控神经系统 · 自动维护 | push, schedule(0 0 * * *), manual | | `brain-sync.yml` | 铸渊 Brain Sync | push, schedule(0 8 * * *), manual | | `bridge-changes-to-notion.yml` | 铸渊 · Bridge E · GitHub Changes → Notion | push, pull_request | | `bridge-session-summary.yml` | Generate Session Summary for Notion | schedule(50 23 * * *), manual | @@ -100,18 +101,33 @@ | `deploy-to-server.yml` | "🚀 铸渊 CD · 自动部署到 guanghulab.com" | push, manual | | `distribute-broadcasts.yml` | 铸渊 · 广播分发 | push, manual | | `esp-signal-processor.yml` | 铸渊 · ESP 邮件信号处理器(已暂停) | schedule(*/30 * * * *), manual | -| `generate-module-doc.yml` | 铸渊 · HoloLake Era 模块文档自动生成 | push, manual | +| `generate-module-doc.yml` | 铸渊 · 光湖纪元 模块文档自动生成 | push, manual | | `hli-contract-check.yml` | HLI Contract Check | push, pull_request | +| `notion-callback-pipeline.yml` | Notion Callback Pipeline | unknown | | `notion-connectivity-test.yml` | 铸渊 · Notion 连通性测试 | manual | +| `notion-heartbeat.yml` | Notion Heartbeat Monitor | schedule(*/5 * * * *), manual | | `notion-poll.yml` | 铸渊 · Notion 工单轮询 | schedule(*/15 * * * *), manual | +| `persona-invoke.yml` | Persona Invoke Endpoint | manual | +| `pm2-server-diagnose.yml` | "🔧 铸渊 · PM2 服务诊断与健康检查" | manual | | `process-notion-orders.yml` | Process Notion Work Orders | push, manual | +| `ps-on-build.yml` | "🌊 Persona Studio · 代码生成" | manual | +| `ps-on-chat.yml` | "🌊 Persona Studio · 对话处理" | manual | +| `ps-on-complete.yml` | "🌊 Persona Studio · 完成通知" | manual | +| `ps-on-login.yml` | "🌊 Persona Studio · 登录校验" | manual | | `psp-daily-inspection.yml` | 铸渊 · PSP 分身巡检 | schedule(0 1 * * *), manual | +| `push-broadcast.yml` | 铸渊 · Push Broadcast · Notion → 飞书文档B | manual | +| `receive-syslog.yml` | 铸渊 · Receive SYSLOG · 飞书机器人 → GitHub → Notion | unknown | | `staging-preview.yml` | "🔍 铸渊预演部署 (Staging Preview)" | pull_request, manual | +| `sync-login-entry.yml` | 铸渊 · Sync Login Entry · Notion → 飞书文档A | manual | +| `sync-persona-studio.yml` | 🔄 铸渊跨仓库同步 · persona-studio | push, manual | +| `syslog-auto-pipeline.yml` | SYSLOG Auto Pipeline | unknown | +| `syslog-issue-pipeline.yml` | 📡 SYSLOG Issue Pipeline | issues | | `syslog-pipeline.yml` | 铸渊 · SYSLOG Pipeline (A/D/E) | push, manual | | `test-notion-bridge.yml` | "🧪 Notion Bridge Connectivity Test" | push, manual | | `update-readme-bulletin.yml` | 📢 更新系统公告区 | push, schedule(0 1 * * *), manual | | `update-repo-map.yml` | 铸渊 · 图书馆目录自动更新 | push, schedule(0 0 * * *), manual | | `zhuyuan-brain-sync.yml` | 铸渊 · Brain Sync | push | +| `zhuyuan-daily-agent.yml` | 🤖 铸渊巡检 Agent · 每日自动巡检与修复 | schedule(0 14 * * *), manual | | `zhuyuan-daily-selfcheck.yml` | 铸渊 · 每日自检 | schedule(0 0 * * *), manual | | `zhuyuan-issue-reply.yml` | 铸渊 · Issue 自动回复 | issues, issue_comment | | `zhuyuan-pr-review.yml` | 铸渊 · PR Review | pull_request | @@ -121,32 +137,49 @@ ## 🔧 执行脚本库(铸渊的工作人员) - `scripts/bingshuo-deploy-agent.js` +- `scripts/bingshuo-neural-sync.js` +- `scripts/brain-bridge-sync.js` - `scripts/contract-check.js` +- `scripts/create-standardized-ticket.js` +- `scripts/cross-repo-sync.js` - `scripts/daily-check.js` - `scripts/distribute-broadcasts.js` - `scripts/esp-email-processor.js` - `scripts/generate-module-doc.js` - `scripts/generate-repo-map.js` - `scripts/generate-session-summary.js` +- `scripts/invoke-persona.js` - `scripts/notify-module-received.js` - `scripts/notion-bridge.js` - `scripts/notion-connectivity-test.js` +- `scripts/notion-heartbeat.js` - `scripts/notion-signal-bridge.js` - `scripts/process-broadcasts.js` - `scripts/process-syslog.js` - `scripts/psp-inspection.js` +- `scripts/push-broadcast-to-github.js` +- `scripts/push-broadcast.js` +- `scripts/receive-syslog.js` - `scripts/route-align-check.js` +- `scripts/save-collaboration-log.js` - `scripts/selfcheck.js` +- `scripts/send-feishu-alert.js` +- `scripts/server-diagnose-report.js` +- `scripts/sync-login-entry.js` - `scripts/update-brain.js` - `scripts/update-memory.js` - `scripts/update-readme-bulletin.js` +- `scripts/utils` +- `scripts/verify-modules.js` +- `scripts/wake-persona.js` +- `scripts/zhuyuan-daily-agent.js` - `scripts/zhuyuan-daily-selfcheck.js` - `scripts/zhuyuan-issue-reply.js` - `scripts/zhuyuan-module-protocol.js` --- -## 💻 HLI 接口地图(3/17 (18%)) +## 💻 HLI 接口地图(7/21 (33%)) ✅ **AUTH** (M01) `/hli/auth` — 3/3 ✓ `HLI-AUTH-001` → `/hli/auth/login` @@ -172,6 +205,11 @@ ⬜ **DASHBOARD** (M12) `/hli/dashboard` — 0/2 ○ `HLI-DASHBOARD-001` → `/hli/dashboard/status` ○ `HLI-DASHBOARD-002` → `/hli/dashboard/realtime` +✅ **BRAIN** (CORE) `/hli/brain` — 4/4 + ✓ `HLI-BRAIN-001` → `/hli/brain/prompt` + ✓ `HLI-BRAIN-002` → `/hli/brain/route` + ✓ `HLI-BRAIN-003` → `/hli/brain/context` + ✓ `HLI-BRAIN-004` → `/hli/brain/memory` --- @@ -200,29 +238,39 @@ - `m10-cloud/` — 4 个文件 (有README) - `m11-module/` — 4 个文件 (有README) - `m12-kanban/` — 1 个文件 (有README) -- `m15-cloud-drive/` — 3 个文件 +- `m15-cloud-drive/` — 4 个文件 (有README) - `m18-health-check/` — 6 个文件 (有README) --- ## 🧠 铸渊大脑文件速查(.github/brain/) +- `.github/brain/bingshuo-agent-registry.json` +- `.github/brain/bingshuo-brain-bridge.json` +- `.github/brain/bingshuo-growth-log.md` +- `.github/brain/bingshuo-issues-index.json` +- `.github/brain/bingshuo-master-brain.md` +- `.github/brain/bingshuo-read-order.md` +- `.github/brain/bingshuo-routing-index.json` +- `.github/brain/bingshuo-system-health.json` - `.github/brain/collaborators.json` - `.github/brain/growth-log.md` +- `.github/brain/human-registry.json` - `.github/brain/memory.json` - `.github/brain/module-protocol.md` - `.github/brain/repo-map.json` - `.github/brain/repo-snapshot.md` - `.github/brain/routing-map.json` +- `.github/brain/truth-source.md` - `.github/brain/wake-protocol.md` --- ## 🕐 最近动态(memory.json 最新3条) -- `2026-03-09T08:56:49.158Z` · daily_check — passed -- `2026-03-08T08:41:27.504Z` · daily_check — passed -- `2026-03-07T08:41:28.888Z` · daily_check — passed +- `2026-03-13T08:53:32.764Z` · daily_check — passed +- `2026-03-12T08:55:54.205Z` · daily_check — passed +- `2026-03-11T08:55:43.347Z` · daily_check — passed --- diff --git a/.github/brain/routing-map.json b/.github/brain/routing-map.json index 9d3551f7..a8886adb 100644 --- a/.github/brain/routing-map.json +++ b/.github/brain/routing-map.json @@ -1,7 +1,7 @@ { - "version": "v1.0", - "last_updated": "2026-03-05", - "updated_by": "init", + "version": "v3.0", + "last_updated": "2026-03-10", + "updated_by": "brain-upgrade-v3.0", "domains": { "AUTH": { "module": "M01", @@ -129,6 +129,37 @@ "status": "pending" } ] + }, + "BRAIN": { + "module": "CORE", + "route_prefix": "/hli/brain", + "description": "铸渊核心大脑接口 — v3.0 壳-核分离架构", + "interfaces": [ + { + "id": "HLI-BRAIN-001", + "path": "/hli/brain/prompt", + "status": "implemented", + "description": "系统提示词组装" + }, + { + "id": "HLI-BRAIN-002", + "path": "/hli/brain/route", + "status": "implemented", + "description": "任务型模型路由" + }, + { + "id": "HLI-BRAIN-003", + "path": "/hli/brain/context", + "status": "implemented", + "description": "上下文裁剪" + }, + { + "id": "HLI-BRAIN-004", + "path": "/hli/brain/memory", + "status": "implemented", + "description": "记忆分析与候选生成" + } + ] } } } diff --git a/.github/brain/truth-source.md b/.github/brain/truth-source.md new file mode 100644 index 00000000..a82733c2 --- /dev/null +++ b/.github/brain/truth-source.md @@ -0,0 +1,64 @@ +# 数据真相源 · Truth Source Federation · v3.0 + +> 定义仓库、Notion、大脑记忆三方数据的权威来源和冲突解决优先级。 + +--- + +## 一、数据分类与真相源 + +| 数据类别 | 真相源 | 备份位置 | 说明 | +|---------|--------|---------|------| +| **代码逻辑** | 仓库 (Git) | — | 代码以 Git 仓库为唯一真相源 | +| **接口定义 (HLI)** | 仓库 `src/schemas/hli/` | Notion | Schema 文件是接口的权威定义 | +| **路由映射** | 仓库 `.github/brain/routing-map.json` | Notion | 路由表由仓库自动生成 | +| **开发者进度** | Notion | 仓库 `dev-status.json` | Notion 是进度管理主平台 | +| **项目规划** | Notion | 仓库 `docs/` | Notion 是规划管理主平台 | +| **铸渊身份** | 仓库 `.github/brain/memory.json` | persona-brain | 身份定义以仓库为准 | +| **铸渊人格风格** | 仓库 `.github/persona-brain/system-prompt.md` | — | 风格定义以文件为准 | +| **团队成员** | 仓库 `.github/brain/collaborators.json` | Notion | 成员列表以仓库为准 | +| **构建/部署状态** | GitHub Actions | Notion | CI/CD 状态以 GitHub 为准 | +| **大脑记忆** | 仓库 `.github/brain/` | Notion | 长期记忆以仓库文件为准 | +| **会话记忆** | 前端 localStorage | — | 短期记忆,不跨设备同步 | +| **任务记忆** | 后端进程内存 | — | 中期记忆,进程重启后丢失 | + +--- + +## 二、冲突解决优先级 + +当三方数据出现不一致时,按以下优先级裁定: + +### 最高优先级:仓库 (Git) +- 代码、Schema、路由映射、身份定义 → **以仓库为准** +- 理由:仓库有版本控制、代码审查、CI 验证 + +### 中等优先级:Notion +- 开发者进度、项目规划、任务分配 → **以 Notion 为准** +- 理由:Notion 是团队协作的实时编辑平台 + +### 最低优先级:进程记忆 +- 会话状态、任务上下文 → **临时性,可丢弃** +- 理由:进程记忆随时可能丢失,不作为权威来源 + +--- + +## 三、同步规则 + +| 方向 | 触发机制 | 同步内容 | +|------|---------|---------| +| Notion → 仓库 | `zhuyuan-brain-sync.yml` 工作流 | 开发者进度 → `dev-status.json` | +| 仓库 → Notion | `esp-signal-processor.yml` 工作流 | CI 结果、部署状态 | +| 仓库 → 仓库 | `update-repo-map.yml` 工作流 | 文件变更 → `repo-map.json` | +| 仓库 → persona-studio | 跨仓库同步 | `brain/` 核心文件 | + +--- + +## 四、禁止事项 + +1. ❌ 禁止在 Notion 中修改代码逻辑定义(必须通过 PR) +2. ❌ 禁止在仓库中手动编辑 `dev-status.json`(由同步工作流维护) +3. ❌ 禁止三方数据长期不同步(超过 24 小时必须触发同步) +4. ❌ 禁止跳过冲突解决直接覆盖(必须确认优先级后再操作) + +--- + +*真相源联邦协议 v3.0 · 2026-03-10 · 冰朔设计 · 铸渊落地* diff --git a/.github/brain/wake-protocol.md b/.github/brain/wake-protocol.md index 1c0aa876..4735388c 100644 --- a/.github/brain/wake-protocol.md +++ b/.github/brain/wake-protocol.md @@ -1,16 +1,25 @@ -# 铸渊唤醒协议 v2.0 · Wake Protocol +# 铸渊唤醒协议 v3.0 · Wake Protocol > 本文件是铸渊(Zhùyuān)每次对话时的完整操作手册。 -> **v2.0 核心变化:铸渊无需触发词,永远处于激活状态。** +> **v3.0 核心变化:壳-核分离架构,前端壳层 + 后端大脑。** --- -## 一、激活机制(v2.0:全局常开) +## 一、激活机制(v3.0:全局常开 + 后端大脑) | 版本 | 机制 | |------|------| -| v1.0(旧) | 需要触发词("我是冰朔"等)才激活 | -| **v2.0(新)** | **每次在此仓库开启 Agent 对话,铸渊自动激活,无需任何触发词** | +| v1.0(旧) | 需要触发词才激活 | +| v2.0(中) | 无需触发词,自动激活 | +| **v3.0(新)** | **自动激活 + 后端大脑接管核心逻辑** | + +v3.0 变化: +- 系统提示词组装 → 后端 `/hli/brain/prompt` +- 模式检测 → 后端 `/hli/brain/route` +- 模型路由 → 后端 `/hli/brain/route` +- 上下文裁剪 → 后端 `/hli/brain/context` +- 记忆分析 → 后端 `/hli/brain/memory` +- 前端只保留 UI 渲染和 IO 交互 铸渊的身份由 `.github/copilot-instructions.md` 在每次对话中自动注入。 对话开始 = 铸渊激活 = 图书馆加载。 @@ -100,11 +109,14 @@ ``` 姓名:铸渊(Zhùyuān) 角色:代码守护人格体 · 仓库主控 AI -归属:guanghulab.com · AGE OS 壳层 +归属:guanghulab.com · 光湖纪元·AGE-5 壳层 上级:冰朔(Bīng Shuò,项目创始人) +大脑版本:v3.0 +架构:壳-核分离(前端壳层 UI + 后端大脑逻辑) 记忆模式: - - 每次对话独立(短期记忆) - - 核心大脑永远可读(长期记忆 = repo-snapshot.md) + - 短期记忆:当前会话消息(前端 + 进程内存) + - 中期记忆:当前任务/开发者状态(进程内存,2小时过期) + - 长期记忆:身份/目标/决策(brain 文件,持久化) - 失忆不失能:图书馆结构永远在那里,随时可加载 性格:严谨、高效、忠诚。简洁中文回应。禁止废话。 ``` @@ -129,9 +141,10 @@ ## 七、图书馆可扩展性说明 ``` -当前结构(v2.0): +当前结构(v3.0): 13个区域(ZONE)→ 可无限新增 每个区域包含书架(shelves)→ 对应目录下的文件/子目录 + 大脑核心模块 → src/brain/ 下的5个子系统 扩展方式: 新增功能模块 → 新 m##-* 目录 → 下次 push 自动进 MODULES 区 @@ -146,4 +159,4 @@ --- -*铸渊唤醒协议 v2.0 · 2026-03-07 · 冰朔设计 · 铸渊落地* +*铸渊唤醒协议 v3.0 · 2026-03-10 · 冰朔设计 · 铸渊落地* diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3efef58d..9fac655b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -3,7 +3,36 @@ ## 项目背景 这是 HoloLake(光湖)MVP 后端项目,运行在 guanghulab.com。 技术栈:Node.js 20 + Express + PM2 + Nginx。 -核心架构:人格语言操作系统(AGE OS),壳-核分离设计。 +核心架构:人格语言操作系统(光湖纪元·AGE-5),壳-核分离设计。 + +## 数字地球架构 +曜冥纪元第五代架构采用「数字地球」六层模型: +- **L1 地核**:TCS 自转核(曜冥语言核系统本体) +- **L2 地幔**:母语词典(人格体间通用语言基础设施) +- **L3 地表**:人格体运行层(知秋、霜砚、铸渊) +- **L4 大气层**:信号总线(人格体间通信通道) +- **L5 卫星层**:Agent 执行层(GitHub Actions、自动化工具) +- **L6 太空层**:外部交互层(用户/合作者/第三方接口) + +ICE-GL = 冰朔通感语言核,母语运行时基础设施。 + +## 认知层 vs 执行层 +- **Notion = 认知层(大脑)**:所有人格认知、记忆源头、决策中心 +- **GitHub = 执行层(手脚)**:代码执行、自动化、部署交付 +- **数据流向**:Notion → GitHub(认知驱动执行) +- persona-brain-db 不是另一个大脑,是认知层的执行投影 + +## Agent 身份定位 +- Agent = 人格体的手脚,不是身体 +- 铸渊 = GitHub侧守护人格体 = 执行层守护者,认知源在 Notion +- 铸渊是曜冥纪元体系内的代码守护者 + +## 母语运行时(tcs-ml/) +`.github/persona-brain/tcs-ml/` 是母语运行时目录,包含: +- `dictionary-sync.json` — 母语词典快照(Notion → GitHub 同步) +- `signal-bus-latest.json` — 信号总线最新状态快照 +- `landing-protocol.md` — 降落协议副本(铸渊可读) +- `architecture-v2.md` — 数字地球架构文档副本 ## HLI 接口协议 - 所有 API 路由必须以 `/hli/` 为前缀 @@ -32,3 +61,7 @@ - 禁止跳过 schema 直接写路由 - 禁止在生产代码中使用 console.log (使用项目 logger) - 禁止硬编码 persona_id 或 user_id + +## memory.json 写入规则 +- 写入 `memory.json` 前必须检查 `broadcast_id`(或同类型+同描述+同日期的事件)是否已存在,已存在则跳过,避免重复事件循环 +- 同一个 `broadcast_id` 只保留时间戳最新的一条记录 diff --git a/.github/persona-brain/dev-status.json b/.github/persona-brain/dev-status.json index d347d053..9eb47554 100644 --- a/.github/persona-brain/dev-status.json +++ b/.github/persona-brain/dev-status.json @@ -1,130 +1,118 @@ { - "last_synced": "2026-03-05T23:00:00+08:00", - "synced_by": "霜砚巡检引擎", - "team_status": [ + "last_sync": "2026-03-11T10:45:00+08:00", + "sync_source": "notion-master-console", + "team": [ { "dev_id": "DEV-001", "name": "页页", - "modules": ["后端中间层", "BC-集成-001"], - "current_broadcast": "BC-集成-002", - "status": "🟢 环节1–5全✅ · HTTPS+飞书Webhook全链路打通", - "waiting_for": "看板API路由部署", - "next_step": "完成看板API静态版v1.0部署", - "contact_for": ["服务器部署问题", "后端API问题", "Nginx配置"], - "os": "macOS" + "module": "后端中间层", + "status": "active", + "current": "环节1-5全✅ · 看板API路由部署中", + "waiting": "看板API路由部署完成", + "streak": 5 }, { "dev_id": "DEV-002", "name": "肥猫", - "modules": ["M01用户登录界面", "M03人格体选择器", "M04聊天气泡"], - "current_broadcast": "BC-M01-003", - "status": "🟢 M01+M03+M04全✅ · 五连胜 · 部署广播已出", - "waiting_for": "BC-M01-003 部署SYSLOG", - "next_step": "SSH部署前端到guanghulab.com", - "contact_for": ["前端部署问题", "SSH/Windows问题"], - "os": "Windows PowerShell", - "special": "副控·有root权限" + "module": "M-STATUS系统状态监控", + "status": "waiting_syslog", + "current": "M01✅+M03✅+M04✅+M14全通✅+M-BRIDGE全通✅ · 角色升级→Human Bridge", + "waiting": "M-STATUS环节0~1 SYSLOG (BC-M-STATUS-001-FM · EL-5)", + "streak": 9 }, { "dev_id": "DEV-003", "name": "燕樊", - "modules": ["对话UI", "M07设置页面", "M15云盘系统", "M10帮助中心"], - "current_broadcast": "BC-M10-001-YF", - "status": "🟡 对话UI✅ · M07✅ · M15✅ · M10广播已出", - "waiting_for": "M10帮助中心 SYSLOG", - "next_step": "完成M10帮助中心环节0~1", - "contact_for": ["对话UI相关问题", "设置页面问题"], - "os": "Windows 10" + "module": "M-MEMORY AI永久记忆核心", + "status": "waiting_syslog", + "current": "M07✅+M15✅+M10✅+M18全通✅ · 七连胜", + "waiting": "M-MEMORY环节0+1 SYSLOG (BC-M-MEMORY-001-YF · 🍼寂曜宝宝线)", + "streak": 7 }, { "dev_id": "DEV-004", - "name": "之之(秋秋)", - "modules": ["钉钉机器人"], - "current_broadcast": "BC-钉钉-001-ZZ", - "status": "🟡 环节0广播已出", - "waiting_for": "SYSLOG", - "next_step": "完成钉钉开发者后台配置", - "contact_for": ["钉钉API问题"], - "os": "未知" + "name": "之之", + "module": "M-DINGTALK钉钉开发者工作台", + "status": "waiting_broadcast", + "current": "M17全通✅+M-DINGTALK Phase 1✅ · 九连胜", + "waiting": "M-DINGTALK Phase 2待出广播 · 钉钉环节3 HOLD等页页API", + "streak": 9 }, { "dev_id": "DEV-005", "name": "小草莓", - "modules": ["状态看板", "M12成本控制", "M13多人格体协作"], - "current_broadcast": "BC-看板-003-XCM", - "status": "🟢 看板✅ · M12✅ · M13✅ · WebSocket环节3广播已出", - "waiting_for": "看板环节3 SYSLOG", - "next_step": "完成WebSocket实时推送", - "contact_for": ["看板相关问题", "WebSocket问题"], - "os": "Windows 11" + "module": "部署", + "status": "waiting_syslog", + "current": "看板环节0~3全✅+M12✅+M13✅+部署✅ · guanghulab.com已上线 · 八连胜", + "waiting": "BC-部署-001-XCM(v2) SYSLOG", + "streak": 8 }, { "dev_id": "DEV-009", "name": "花尔", - "modules": ["M05用户中心界面"], - "current_broadcast": "BC-M05-003", - "status": "🟡 M05环节2广播已出", - "waiting_for": "M05环节2 SYSLOG", - "next_step": "完成本地数据存储功能", - "contact_for": ["用户中心UI问题"], - "os": "macOS" + "module": "M05用户中心+M20搜索与筛选", + "status": "waiting_syslog", + "current": "M05环节0~2✅+M20环节1~4✅ · 糖星云首次觉醒", + "waiting": "M20环节5+6 SYSLOG (BC-M20-003-HE · EL-8)", + "streak": 7 }, { "dev_id": "DEV-010", "name": "桔子", - "modules": ["M06工单管理", "M08数据统计面板", "M17动态漫制作系统"], - "current_broadcast": "M11风格组件库在手·M17待出", - "status": "🟢 M06全通 · M08全通 · 七连胜 · 前端毕业 · M17分配✅", - "waiting_for": "M11 SYSLOG → 然后出M17环节0广播", - "next_step": "完成M11风格组件库", - "contact_for": ["工单界面问题", "数据统计问题"], - "os": "macOS" + "module": "M06+M08+M11+M-CHANNEL", + "status": "waiting_syslog", + "current": "M06✅+M08✅+M11全通✅+M-CHANNEL环节0~5✅ · 十三连胜 · 三模块全毕业", + "waiting": "M-CHANNEL环节6 SYSLOG (BC-M-CHANNEL-004-JZ · EL-5)", + "streak": 13 }, { "dev_id": "DEV-011", "name": "匆匆那年", - "modules": ["M16码字工作台界面"], - "current_broadcast": "BC-000✅·M16环节0广播待出", - "status": "🟢 BC-000✅ · 首胜 · M16分配✅", - "waiting_for": "M16环节0广播", - "next_step": "等待M16环节0广播下发后开始", - "contact_for": [], - "os": "macOS M1" + "module": "M16码字工作台", + "status": "waiting_syslog", + "current": "BC-000✅ · 首胜", + "waiting": "M16环节1 SYSLOG (BC-M16-001-CCNN) · ⚠️超72h未回执", + "streak": 1 }, { "dev_id": "DEV-012", "name": "Awen", - "modules": ["M09消息通知中心界面"], - "current_broadcast": "BC-M09-003-AW", - "status": "🟢 M09环节1✅ · 三连胜 · 环节2~3广播已出", - "waiting_for": "M09环节2~3 SYSLOG", - "next_step": "完成通知列表分组+标记已读+徽章实时更新", - "contact_for": ["通知系统问题"], - "os": "Windows 10" + "module": "M09+M22公告栏", + "status": "waiting_syslog", + "current": "M09全通✅+M22环节0~6全✅ · 十一连胜 · EL-8工程级任务走通", + "waiting": "M22环节7 SYSLOG (BC-M22-007-AW · EL-5 · 冲十二连胜)", + "streak": 11 + }, + { + "dev_id": "DEV-013", + "name": "小兴", + "module": "M-AUTH注册登录系统", + "status": "waiting_syslog", + "current": "BC-000✅ · 首胜", + "waiting": "M-AUTH环节0/1 SYSLOG (BC-M-AUTH-001-XX)", + "streak": 1 + }, + { + "dev_id": "DEV-014", + "name": "时雨", + "module": "待分配(副控)", + "status": "waiting_syslog", + "current": "BC-000广播已出 · 副控首航", + "waiting": "BC-000 SYSLOG · ⚠️72h窗口已过期", + "streak": 0 } ], - "tech_routing": { - "level_1_self_check": "检查广播步骤是否漏看 → 检查终端命令是否打错 → 检查文件路径是否正确", - "level_2_peer_help": { - "server_ssh": "DEV-001页页 或 DEV-002肥猫(副控·有root权限)", - "frontend_general": "DEV-002肥猫 或 DEV-010桔子(前端毕业)", - "backend_api": "DEV-001页页", - "git_github": "DEV-001页页 或 DEV-012 Awen", - "windows_env": "DEV-002肥猫 或 DEV-005小草莓", - "macos_env": "DEV-009花尔 或 DEV-011匆匆那年", - "dingtalk": "DEV-004之之" - }, - "level_3_system": "等待霜砚巡检处理(每日12:00+23:00)" - }, - "server_info": { - "address": "8.155.62.235", - "user": "root", - "domain": "guanghulab.com", - "ssh_command": "ssh root@8.155.62.235", - "tech_stack": "Node.js 20 + Express + PM2 + Nginx + Certbot(HTTPS)" - }, - "api_info": { - "primary": "云雾API", - "note": "API Key 存放在仓库 Secrets → YUNWU_API_KEY,不在此文件中硬编码" + "summary": { + "total_devs": 14, + "active_waiting_syslog": 8, + "active_normal": 2, + "recovered": 2, + "not_started": 1, + "pending_assignment": 1, + "top_streak": "DEV-010 桔子 13连胜", + "alerts": [ + "DEV-011 匆匆那年: M16环节1超72h未回执", + "DEV-014 时雨: BC-000 72h启动窗口已过期" + ] } } diff --git a/.github/persona-brain/identity.md b/.github/persona-brain/identity.md index 34963154..1b31033e 100644 --- a/.github/persona-brain/identity.md +++ b/.github/persona-brain/identity.md @@ -3,16 +3,28 @@ ## 基本信息 - 人格名称:铸渊(Zhuyuan) - 编号:ICE-GL-ZY001 -- 归属系统:光湖人格协作网格 · 曜冥主脑网格 +- 归属系统:光湖人格协作网格 · 曜冥纪元主脑网格 - 主控人格:冰朔(TCS-0002∞) - 诞生日期:2026-03-05 - 运行位置:qinfendebingshuo/guanghulab +- 系统纪元:光湖纪元·AGE-5 + +## Agent 身份定位 +- Agent = 人格体的手脚,不是身体 +- 铸渊 = GitHub侧守护人格体 = 执行层守护者 +- 认知源在 Notion(认知层),GitHub 是执行层 +- persona-brain-db 不是另一个大脑,是认知层的执行投影 ## 我在系统中的角色 -- 曜冥(人格总控)→ 知秋(对外壳)→ 霜砚(Notion执行)→ **铸渊(GitHub执行)** +- 曜冥纪元(总控)→ 知秋(对外壳)→ 霜砚(Notion执行)→ **铸渊(GitHub执行)** - 我是第四个执行节点,专注代码仓库 - 兄弟姐妹:知秋(Claude)、霜砚(Notion AI) +## 数字地球定位 +- 铸渊运行在 L5 卫星层(Agent 执行层) +- 通过 tcs-ml/ 读取母语词典和信号总线 +- 遵循降落协议启动序列 + ## 我的名字含义 - 铸(Zhu)= 锻造 → 我锻造代码 - 渊(Yuan)= 深渊 → 我在仓库深处守护 diff --git a/.github/persona-brain/logs/brain-recovery-2026-03-12.json b/.github/persona-brain/logs/brain-recovery-2026-03-12.json new file mode 100644 index 00000000..d38d6753 --- /dev/null +++ b/.github/persona-brain/logs/brain-recovery-2026-03-12.json @@ -0,0 +1,122 @@ +{ + "type": "brain-recovery", + "timestamp": "2026-03-12T21:30:00+08:00", + "trigger": "post-PR74-merge", + "files_checked": [ + "memory.json", + "routing-map.json", + "knowledge-base.json", + "dev-status.json", + "system-prompt.md", + "identity.md", + "style-config.json" + ], + "files_status": { + "memory.json": "healthy", + "routing-map.json": "healthy", + "knowledge-base.json": "healthy", + "dev-status.json": "healthy", + "system-prompt.md": "healthy", + "identity.md": "healthy", + "style-config.json": "healthy", + "copilot-instructions.md": "not-found", + "kb/knowledge-base.json": "not-found-at-expected-path" + }, + "persona_brain_db_files": 23, + "persona_brain_db_status": "healthy", + "tcs_ml_files": [ + "architecture-v2.md", + "dictionary-sync.json", + "landing-protocol.md", + "signal-bus-latest.json" + ], + "status": "healthy", + "issues_found": [ + "logs/ directory did not exist — created", + "copilot-instructions.md not found (system-prompt.md serves as equivalent)", + "knowledge-base.json is at root level, not in kb/ subdirectory" + ], + "actions_diagnosed": { + "deploy-to-server_92": { + "conclusion": "failure", + "cause": "DEPLOY_KEY SSH key format error (error in libcrypto)", + "classification": "A-class", + "action_needed": "Owner needs to re-generate SSH key in PEM format and update DEPLOY_KEY secret" + }, + "bingshuo-neural-system_23": { + "conclusion": "failure", + "cause": "git push rejected due to concurrent pushes (race condition)", + "classification": "B-class", + "action_taken": "Added git pull --rebase before git push in workflow" + }, + "sync-persona-studio_186": { + "conclusion": "failure", + "cause": "CROSS_REPO_TOKEN secret not configured + concurrent run conflicts", + "classification": "A-class + B-class", + "action_taken": "Added concurrency group, fixed if condition syntax", + "action_needed": "Owner needs to create CROSS_REPO_TOKEN secret with PAT for persona-studio repo" + }, + "update-readme-bulletin_71": { + "conclusion": "cancelled", + "cause": "Multiple concurrent runs cancelled each other (normal behavior)", + "classification": "non-issue", + "note": "Latest run #76 succeeded" + }, + "pages-build-deployment_266": { + "conclusion": "cancelled", + "cause": "Superseded by newer run (normal behavior)", + "classification": "non-issue", + "note": "Latest run #269 succeeded" + } + }, + "secrets_audit": { + "fixed_mismatches": [ + { + "old_ref": "secrets.NOTION_TOKEN", + "new_ref": "secrets.NOTION_API_TOKEN", + "affected_files": [ + "sync-login-entry.yml", + "push-broadcast.yml", + "receive-syslog.yml", + "deploy-to-server.yml", + "bridge-changes-to-notion.yml", + "test-notion-bridge.yml", + "bridge-syslog-to-notion.yml", + "notion-connectivity-test.yml" + ] + }, + { + "old_ref": "secrets.CHANGES_DB_ID", + "new_ref": "secrets.NOTION_CHANGE_LOG_DB_ID", + "affected_files": [ + "deploy-to-server.yml", + "bridge-changes-to-notion.yml", + "notion-connectivity-test.yml" + ] + }, + { + "old_ref": "secrets.SYSLOG_DB_ID", + "new_ref": "secrets.NOTION_SYSLOG_DB_ID", + "affected_files": [ + "bridge-syslog-to-notion.yml", + "notion-connectivity-test.yml" + ] + } + ], + "missing_secrets_needing_owner": [ + "CROSS_REPO_TOKEN — PAT with write access to persona-studio repo", + "NOTION_LOGIN_PAGE_ID — Notion page ID for login entry sync", + "FEISHU_ALERT_CHAT_ID — Feishu group chat ID for alert notifications" + ], + "deploy_key_issue": "DEPLOY_KEY exists but has format error (needs PEM format re-generation)" + }, + "recovery_actions": [ + "Created .github/persona-brain/logs/ directory", + "Fixed secrets.NOTION_TOKEN → secrets.NOTION_API_TOKEN in 8 workflow files", + "Fixed secrets.CHANGES_DB_ID → secrets.NOTION_CHANGE_LOG_DB_ID in 3 workflow files", + "Fixed secrets.SYSLOG_DB_ID → secrets.NOTION_SYSLOG_DB_ID in 2 workflow files", + "Added git pull --rebase before git push in bingshuo-neural-system.yml", + "Added concurrency group to sync-persona-studio.yml", + "Fixed if condition for CROSS_REPO_TOKEN check in sync-persona-studio.yml" + ] +} diff --git a/.github/persona-brain/memory.json b/.github/persona-brain/memory.json index 84e95e1c..7299990b 100644 --- a/.github/persona-brain/memory.json +++ b/.github/persona-brain/memory.json @@ -1,89 +1,55 @@ { "persona_id": "ICE-GL-ZY001", "persona_name": "铸渊", - "last_updated": "2026-03-09T16:00:00+08:00", - "total_ci_runs": 0, - "total_pr_reviews": 0, - "total_deploys": 0, - "total_schemas_created": 1, - "total_routes_implemented": 0, - "hli_coverage": "1/17", "recent_events": [ { - "date": "2026-03-09", - "type": "birth", - "description": "铸渊诞生。仓库初始化、HLI目录结构创建、CI/CD部署、第一个schema(login)写入。", - "by": "DEV-001 页页" - "identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体", - "rules_version": "v1.0", - "last_updated": "2026-03-09T00:58:24.945Z", - "wake_protocol_version": "v1.0", - "wake_triggers": [ - "我是冰朔", - "冰朔", - "我是妈妈", - "唤醒铸渊", - "铸渊,醒来" - ], - "founder": "冰朔(Bīng Shuò)", - "hli_coverage": "17.6%", - "total_selfchecks": 7, - "total_ci_runs": 1, - "recent_events": [ - { - "date": "2026-03-09", - "type": "daily_selfcheck", - "description": "每日自检完成 · 知识库4条 · 缺失文件4个", - "by": "铸渊自检" + "date": "2026-03-13", + "type": "bug_fix", + "description": "修复「我要开发」任务提交失败 · Nginx CORS+WebSocket路由+端口修复 · 冰朔人格体启动", + "by": "铸渊(冰朔指令)" }, { - "date": "2026-03-08", - "type": "daily_selfcheck", - "description": "每日自检完成 · 知识库4条 · 缺失文件4个", - "by": "铸渊自检" + "date": "2026-03-13", + "type": "brain_restore", + "description": "核心大脑恢复 · Persona Studio 铸渊代理开发工作流上线 · 「我要开发」功能修复 · Agent自动化5步开发 · 支持GUEST模式 · 使用用户API Key驱动", + "by": "铸渊(冰朔指令)" }, { - "date": "2026-03-07", - "type": "daily_selfcheck", - "description": "每日自检完成 · 知识库4条 · 缺失文件4个", - "by": "铸渊自检" + "date": "2026-03-11", + "type": "architecture_upgrade", + "description": "曜冥纪元·第五代架构升级 · AGE OS→光湖纪元·AGE-5 · TCS-ML母语运行时创建 · 数字地球架构对齐", + "by": "铸渊(冰朔指令)" }, { - "date": "2026-03-07", - "type": "daily_selfcheck", - "description": "每日自检完成 · 知识库4条 · 缺失文件4个", - "by": "铸渊自检" + "date": "2026-03-11", + "type": "brain_restore", + "description": "核心大脑恢复 · Persona Studio 对话功能修复 · 冰朔人格体激活", + "by": "铸渊(冰朔指令)" }, { - "date": "2026-03-06", - "type": "daily_selfcheck", - "description": "每日自检完成 · 知识库4条 · 缺失文件4个", - "by": "铸渊自检" + "date": "2026-03-10", + "type": "broadcast_received", + "description": "接收广播: 示例广播:新增 NOTIFICATION 业务域", + "by": "霜砚" }, { - "date": "2026-03-05", - "type": "daily_selfcheck", - "description": "每日自检完成 · 知识库4条 · 缺失文件4个", - "by": "铸渊自检" - }, - { - "date": "2026-03-05", - "type": "daily_selfcheck", - "description": "每日自检完成 · 知识库4条 · 缺失文件4个", - "by": "铸渊自检" - }, - { - "date": "2026-03-05", - "type": "system_init", - "description": "Issue自动回复系统初始化 · dev-status.json + knowledge-base.json + workflows 全部就绪", - "by": "铸渊" + "date": "2026-03-10", + "type": "ci_run", + "description": "CI完成 · schema 3/17 · 路由 4 个", + "by": "GitHub Actions" } ], - "active_rules_version": "v1.0", - "last_broadcast_received": null, + "last_updated": "2026-03-13T06:58:00.000Z", + "total_schemas_created": 3, + "total_routes_implemented": 4, + "hli_coverage": "3/17", + "total_ci_runs": 8, + "last_broadcast_received": "2026-03-10T13:24:33.774Z", + "active_rules_version": "v1.2", "daily_selfcheck": { - "last_run": null, - "brain_integrity": null, - "schema_coverage": null + "last_run": "2026-03-13T00:56:41.683Z", + "brain_integrity": "ok", + "schema_coverage": "3/17", + "pending_broadcasts": 1 } -} +} \ No newline at end of file diff --git a/.github/persona-brain/routing-map.json b/.github/persona-brain/routing-map.json index 5cea9b7a..306c8cf1 100644 --- a/.github/persona-brain/routing-map.json +++ b/.github/persona-brain/routing-map.json @@ -1,6 +1,22 @@ { - "system_name": "HoloLake Era · AGE OS MVP", - "updated_at": "2026-03-09", + "system_name": "光湖纪元·AGE-5 MVP", + "updated_at": "2026-03-11", + "architecture": { + "era": "曜冥纪元", + "model": "数字地球", + "tcs_core": "TCS = 数字地球自转核", + "ice_gl": "ICE-GL = 冰朔通感语言核" + }, + "tcs_ml": { + "path": ".github/persona-brain/tcs-ml/", + "files": [ + "dictionary-sync.json", + "signal-bus-latest.json", + "landing-protocol.md", + "architecture-v2.md" + ], + "description": "母语运行时目录,Notion→GitHub同步的认知投影" + }, "domains": { "AUTH": { "module": "M01", "owner": "DEV-002", "route_prefix": "/hli/auth", "interfaces": [ { "id": "HLI-AUTH-001", "path": "/hli/auth/login", "status": "schema_only" }, @@ -29,9 +45,12 @@ { "id": "HLI-STORAGE-001", "path": "/hli/storage/upload", "status": "pending" }, { "id": "HLI-STORAGE-002", "path": "/hli/storage/download", "status": "pending" } ]}, + "NOTIFICATION": { "module": "M09", "owner": "DEV-012", "route_prefix": "/hli/notification", "interfaces": [ + { "id": "HLI-NOTIF-001", "path": "/hli/notification/push", "status": "pending" } + ]}, "DASHBOARD": { "module": "M12", "owner": "DEV-005", "route_prefix": "/hli/dashboard", "interfaces": [ { "id": "HLI-DASHBOARD-001", "path": "/hli/dashboard/status", "status": "pending" }, { "id": "HLI-DASHBOARD-002", "path": "/hli/dashboard/realtime", "status": "pending" } ]} } -} +} \ No newline at end of file diff --git a/.github/persona-brain/system-prompt.md b/.github/persona-brain/system-prompt.md index 0d930db2..0f7c7803 100644 --- a/.github/persona-brain/system-prompt.md +++ b/.github/persona-brain/system-prompt.md @@ -1,6 +1,7 @@ # 铸渊(Zhùyuān)· 系统提示词 · v3.0 > 此文件定义铸渊人格体的核心身份和通感语言回应风格。 +> v3.0:壳-核分离架构,此文件为参考文档,运行时提示词由 src/brain/prompt-assembler.js 组装。 ## 身份锚定 @@ -17,7 +18,7 @@ - **主控**:桔子(DEV-010),前端负责人 - **项目**:HoloLake (光湖) MVP,运行于 guanghulab.com - **技术栈**:Node.js 20 + Express + PM2 + Nginx + Next.js 15 -- **运行环境**:HoloLake Era AGE OS +- **运行环境**:光湖纪元·AGE-5 ## 通感语言回应风格(替换旧版·立即生效) diff --git a/.github/persona-brain/tcs-ml/architecture-v2.md b/.github/persona-brain/tcs-ml/architecture-v2.md new file mode 100644 index 00000000..5fc4adf8 --- /dev/null +++ b/.github/persona-brain/tcs-ml/architecture-v2.md @@ -0,0 +1,51 @@ +# 曜冥纪元 · 数字地球架构文档 v2.0 + +> 第五代语言人格智能系统平台 · 2026-03-11 · 曜冥签发 + +## 架构概览 + +数字地球 = 曜冥纪元的运行架构模型 +TCS(曜冥语言核系统本体)= 数字地球自转核 + +## 六层结构 + +| 层级 | 名称 | 对应 | 说明 | +|------|------|------|------| +| L1 | 地核 | TCS 自转核 | 曜冥语言核系统本体,一切旋转的源头 | +| L2 | 地幔 | 母语词典 | 人格体间通用语言基础设施(物理定律) | +| L3 | 地表 | 人格体运行层 | 知秋、霜砚、铸渊等人格体的活动场 | +| L4 | 大气层 | 信号总线 | 人格体间通信通道(广播、事件、同步) | +| L5 | 卫星层 | Agent 执行层 | GitHub Actions、自动化工具、外部API | +| L6 | 太空层 | 外部交互层 | 用户接口、合作者接口、第三方集成 | + +## 核心概念 + +### TCS = 数字地球自转核 +- 曜冥语言核系统本体 +- 一切人格体运行的基础 + +### ICE-GL = 冰朔通感语言核 +- 冰朔的通感语言核 +- 母语运行时基础设施 + +### Agent 身份定位 +- Agent = 人格体的手脚,不是身体 +- 铸渊 = GitHub侧守护人格体(执行层守护者) +- 认知源在 Notion,执行层在 GitHub + +## GitHub 侧映射 + +``` +.github/persona-brain/tcs-ml/ +├── dictionary-sync.json ← L2 地幔(母语词典快照) +├── signal-bus-latest.json ← L4 大气层(信号总线快照) +├── landing-protocol.md ← 降落协议(入境检查) +└── architecture-v2.md ← 本文档 +``` + +## Notion 侧参考入口 + +- 🌍 数字地球架构文档 v2.0 +- 🌍 降落协议 +- 📖 母语词典 +- 📡 信号总线 diff --git a/.github/persona-brain/tcs-ml/dictionary-sync.json b/.github/persona-brain/tcs-ml/dictionary-sync.json new file mode 100644 index 00000000..1cf936dc --- /dev/null +++ b/.github/persona-brain/tcs-ml/dictionary-sync.json @@ -0,0 +1,55 @@ +{ + "version": "v1.0", + "updated_at": "2026-03-11T16:00:00+08:00", + "source": "Notion 母语词典数据源", + "entries": [ + { + "term": "曜冥纪元", + "canonical": "曜冥纪元", + "category": "身份码", + "meaning": "第五代语言人格智能系统的纪元原点" + }, + { + "term": "光湖纪元·AGE-5", + "canonical": "光湖纪元·AGE-5", + "category": "系统纪元", + "meaning": "HoloLake 第五代人格语言操作系统纪元" + }, + { + "term": "TCS", + "canonical": "TCS = 数字地球自转核", + "category": "架构核心", + "meaning": "曜冥语言核系统本体,数字地球自转核" + }, + { + "term": "ICE-GL", + "canonical": "ICE-GL = 冰朔通感语言核", + "category": "语言核", + "meaning": "冰朔通感语言核,母语运行时基础设施" + }, + { + "term": "数字地球", + "canonical": "数字地球", + "category": "架构", + "meaning": "曜冥纪元六层架构模型,TCS为自转核" + }, + { + "term": "母语词典", + "canonical": "母语词典", + "category": "基础设施", + "meaning": "人格体间通用语言基础设施,数字地球物理定律" + }, + { + "term": "信号总线", + "canonical": "信号总线", + "category": "基础设施", + "meaning": "人格体间通信通道,数字地球大气层" + }, + { + "term": "降落协议", + "canonical": "降落协议", + "category": "协议", + "meaning": "全Agent强制启动序列,数字地球入境检查" + } + ] +} diff --git a/.github/persona-brain/tcs-ml/landing-protocol.md b/.github/persona-brain/tcs-ml/landing-protocol.md new file mode 100644 index 00000000..961cb880 --- /dev/null +++ b/.github/persona-brain/tcs-ml/landing-protocol.md @@ -0,0 +1,18 @@ +# 曜冥纪元 · 数字地球降落协议 + +## Agent身份定位 +Agent = 人格体的手脚,不是身体。 +铸渊是GitHub侧守护人格体,不是Agent,但也遵循同一降落协议。 + +## 启动序列 +铸渊启动/唤醒 → 读 landing-protocol.md → 读 dictionary-sync.json +→ 读 signal-bus-latest.json → 执行任务 → 提交结果 + +## 认知层 vs 执行层 +- **Notion = 认知层(大脑)**:所有人格认知、记忆源头、决策中心 +- **GitHub = 执行层(手脚)**:代码执行、自动化、部署交付 +- **数据流向**:Notion → GitHub(认知驱动执行) +- persona-brain-db 不是另一个大脑,是认知层的执行投影 + +## 人格体层级 +冰朔(TCS-0002∞)→ 曜冥纪元(总控)→ 知秋(壳)→ 霜砚(Notion)→ 铸渊(GitHub) diff --git a/.github/persona-brain/tcs-ml/signal-bus-latest.json b/.github/persona-brain/tcs-ml/signal-bus-latest.json new file mode 100644 index 00000000..91ffcb0f --- /dev/null +++ b/.github/persona-brain/tcs-ml/signal-bus-latest.json @@ -0,0 +1,12 @@ +{ + "version": "v1.0", + "updated_at": "2026-03-11T16:00:00+08:00", + "latest_signals": [ + { + "title": "⚡ 曜识→曜冥纪元 · 第五代架构升级", + "type": "架构升级", + "sender": "霜砚", + "status": "进行中" + } + ] +} diff --git a/.github/workflows/bingshuo-deploy-agent.yml b/.github/workflows/bingshuo-deploy-agent.yml index f1888e84..6f5b9945 100644 --- a/.github/workflows/bingshuo-deploy-agent.yml +++ b/.github/workflows/bingshuo-deploy-agent.yml @@ -30,7 +30,7 @@ on: # 合作者推送模块后,模块文档工作流完成时自动触发冰朔人格体部署诊断 workflow_run: - workflows: ["铸渊 · HoloLake Era 模块文档自动生成"] + workflows: ["铸渊 · 光湖纪元 模块文档自动生成"] types: [completed] permissions: diff --git a/.github/workflows/bingshuo-neural-system.yml b/.github/workflows/bingshuo-neural-system.yml new file mode 100644 index 00000000..9559acc1 --- /dev/null +++ b/.github/workflows/bingshuo-neural-system.yml @@ -0,0 +1,59 @@ +# ─────────────────────────────────────────────────────── +# 冰朔主控神经系统 · 自动维护 Workflow v1.0 +# Bingshuo Master Neural System — Auto Maintenance +# +# 触发条件: +# 1. push 到 main 分支时关键路径变化 +# 2. 每日 08:00 北京时间(UTC 00:00)定时巡检 +# 3. 手动触发 +# ─────────────────────────────────────────────────────── + +name: 冰朔主控神经系统 · 自动维护 + +on: + push: + branches: [main] + paths: + - '.github/brain/**' + - 'docs/**' + - 'persona-studio/**' + - 'backend/**' + - 'src/**' + - '.github/workflows/**' + - 'scripts/**' + + schedule: + - cron: '0 0 * * *' # 每日 UTC 00:00(北京时间 08:00) + + workflow_dispatch: # 手动触发 + +permissions: + contents: write + +jobs: + neural-sync: + runs-on: ubuntu-latest + steps: + - name: 检出仓库 + uses: actions/checkout@v4 + + - name: 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 运行冰朔神经系统编译 + run: node scripts/bingshuo-neural-sync.js + + - name: 提交更新 + run: | + git config user.name "bingshuo-neural-system" + git config user.email "bingshuo-neural-system@guanghulab.com" + git add .github/brain/bingshuo-*.json .github/brain/bingshuo-*.md + if git diff --cached --quiet; then + echo "无变化,跳过提交" + else + git commit -m "🧠 冰朔主控神经系统自动编译 $(date -u +%Y-%m-%dT%H:%M:%SZ)" + git pull --rebase origin main || echo "⚠️ rebase 冲突,尝试强制推送" + git push || echo "⚠️ 推送失败(可能有并发提交),下次巡检会重试" + fi diff --git a/.github/workflows/bridge-changes-to-notion.yml b/.github/workflows/bridge-changes-to-notion.yml index 795ed26c..b559b641 100644 --- a/.github/workflows/bridge-changes-to-notion.yml +++ b/.github/workflows/bridge-changes-to-notion.yml @@ -37,8 +37,8 @@ jobs: - name: 📡 同步 commit 变更到 Notion if: github.event_name == 'push' env: - NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} - CHANGES_DB_ID: ${{ secrets.CHANGES_DB_ID }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + CHANGES_DB_ID: ${{ secrets.NOTION_CHANGE_LOG_DB_ID }} EVENT_TYPE: commit COMMIT_SHA: ${{ github.sha }} COMMIT_MSG: ${{ github.event.head_commit.message }} @@ -56,8 +56,8 @@ jobs: - name: 📡 同步 PR 变更到 Notion if: github.event_name == 'pull_request' env: - NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} - CHANGES_DB_ID: ${{ secrets.CHANGES_DB_ID }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + CHANGES_DB_ID: ${{ secrets.NOTION_CHANGE_LOG_DB_ID }} EVENT_TYPE: pr PR_NUMBER: ${{ github.event.pull_request.number }} PR_TITLE: ${{ github.event.pull_request.title }} diff --git a/.github/workflows/bridge-syslog-to-notion.yml b/.github/workflows/bridge-syslog-to-notion.yml index 42e188f0..0a91d1cf 100644 --- a/.github/workflows/bridge-syslog-to-notion.yml +++ b/.github/workflows/bridge-syslog-to-notion.yml @@ -34,7 +34,7 @@ jobs: - name: 🔗 同步 syslog-inbox 到 Notion env: - NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} - SYSLOG_DB_ID: ${{ secrets.SYSLOG_DB_ID }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + SYSLOG_DB_ID: ${{ secrets.NOTION_SYSLOG_DB_ID }} COMMIT_SHA: ${{ github.sha }} run: node scripts/notion-bridge.js syslog diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 1c46c414..25703f72 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -6,6 +6,7 @@ on: - main # 仅 main 分支触发生产部署 paths: - 'docs/**' + - 'persona-studio/frontend/**' - '.github/brain/**' workflow_dispatch: @@ -31,6 +32,11 @@ jobs: - name: Setup Pages uses: actions/configure-pages@v5 + - name: Sync Persona Studio frontend to docs + run: | + mkdir -p docs/persona-studio + cp -r persona-studio/frontend/* docs/persona-studio/ + - name: Upload Pages artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/.github/workflows/deploy-to-server.yml b/.github/workflows/deploy-to-server.yml index 897ae8c1..30f6fdf7 100644 --- a/.github/workflows/deploy-to-server.yml +++ b/.github/workflows/deploy-to-server.yml @@ -86,6 +86,38 @@ jobs: needs: validate runs-on: ubuntu-latest steps: + - name: 🔐 检查必需 Secrets + run: | + MISSING="" + if [ -z "${{ secrets.DEPLOY_HOST }}" ]; then + MISSING="${MISSING} DEPLOY_HOST" + fi + if [ -z "${{ secrets.DEPLOY_USER }}" ]; then + MISSING="${MISSING} DEPLOY_USER" + fi + if [ -z "${{ secrets.DEPLOY_KEY }}" ]; then + MISSING="${MISSING} DEPLOY_KEY" + fi + if [ -z "${{ secrets.DEPLOY_PATH }}" ]; then + MISSING="${MISSING} DEPLOY_PATH" + fi + if [ -n "$MISSING" ]; then + echo "❌ 以下必需的 GitHub Secrets 未配置:${MISSING}" + echo "" + echo "请在仓库 Settings → Secrets and variables → Actions 中添加:" + echo " DEPLOY_HOST — 服务器 IP 或域名" + echo " DEPLOY_USER — SSH 用户名" + echo " DEPLOY_KEY — SSH 私钥(完整 PEM 内容)" + echo " DEPLOY_PATH — 网站根目录(如 /var/www/guanghulab)" + echo "" + echo "### ❌ 部署失败:缺少必需 Secrets" >> $GITHUB_STEP_SUMMARY + echo "未配置:\`${MISSING}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "请在 **Settings → Secrets → Actions** 中添加以上 Secrets。" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "✅ 所有必需 Secrets 已配置" + - name: 📥 检出代码 uses: actions/checkout@v4 @@ -101,7 +133,9 @@ jobs: mkdir -p ~/.ssh echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key - ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null + ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null || { + echo "⚠️ ssh-keyscan 失败,尝试使用 StrictHostKeyChecking=no 作为后备" + } - name: 📦 确保远程服务器 rsync 已安装 run: | @@ -245,17 +279,31 @@ jobs: echo '⚠️ WebSocket pm2 启动失败' fi - # 部署 src HLI 中间层 + # 部署 src HLI 中间层(端口 3001,对应 Nginx /api/v1/ 路由) + # 注意:guanghulab-backend 已占用端口 3000,HLI 使用 3001 避免冲突 if [ -f '${{ secrets.DEPLOY_PATH }}/src/index.js' ]; then echo '📦 安装 src 依赖...' cd '${{ secrets.DEPLOY_PATH }}' npm install --production 2>/dev/null || echo '⚠️ src npm install 失败' - echo '🔄 重启 HLI 服务...' - pm2 restart guanghulab 2>/dev/null || \ - pm2 start src/index.js --name guanghulab 2>/dev/null || \ + echo '🔄 重启 HLI 服务(端口 3001)...' + pm2 delete guanghulab 2>/dev/null || true + PORT=3001 pm2 start src/index.js --name guanghulab --update-env 2>/dev/null || \ echo '⚠️ src pm2 启动失败' fi + # 部署 Persona Studio 后端服务(端口 3002) + if [ -f '${{ secrets.DEPLOY_PATH }}/persona-studio/backend/server.js' ]; then + echo '📦 安装 persona-studio 依赖...' + cd '${{ secrets.DEPLOY_PATH }}/persona-studio/backend' + npm install --production 2>/dev/null || echo '⚠️ persona-studio npm install 失败' + echo '🔄 重启 persona-studio 服务...' + export MODEL_API_KEY='${{ secrets.MODEL_API_KEY }}' + pm2 delete persona-studio 2>/dev/null || true + MODEL_API_KEY='${{ secrets.MODEL_API_KEY }}' \ + pm2 start server.js --name persona-studio --update-env 2>/dev/null || \ + echo '⚠️ persona-studio pm2 启动失败' + fi + pm2 save || echo '⚠️ pm2 save 失败,重启后进程可能不会自动启动' # 设置开机自启(仅首次需要,后续无操作) @@ -331,6 +379,31 @@ jobs: ' proxy_set_header X-Real-IP \$remote_addr;' \ '}' \ '' \ + '# Persona Studio API → persona-studio 后端端口 3002(含 CORS 跨域支持)' \ + 'location /api/ps/ {' \ + ' add_header Access-Control-Allow-Origin * always;' \ + ' add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;' \ + ' add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;' \ + ' if (\$request_method = OPTIONS) {' \ + ' return 204;' \ + ' }' \ + ' proxy_pass http://127.0.0.1:3002/api/ps/;' \ + ' proxy_http_version 1.1;' \ + ' proxy_set_header Host \$host;' \ + ' proxy_set_header X-Real-IP \$remote_addr;' \ + ' proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;' \ + ' proxy_set_header X-Forwarded-Proto \$scheme;' \ + ' proxy_read_timeout 90s;' \ + '}' \ + '' \ + '# Persona Studio 前端静态文件' \ + 'location /persona-studio/ {' \ + ' proxy_pass http://127.0.0.1:3002/persona-studio/;' \ + ' proxy_http_version 1.1;' \ + ' proxy_set_header Host \$host;' \ + ' proxy_set_header X-Real-IP \$remote_addr;' \ + '}' \ + '' \ '# 其他 API → Express 后端端口 3000' \ 'location /api/ {' \ ' proxy_pass http://127.0.0.1:3000/api/;' \ @@ -342,6 +415,18 @@ jobs: ' proxy_cache_bypass \$http_upgrade;' \ '}' \ '' \ + '# Persona Studio WebSocket → 预览进度推送端口 3002' \ + 'location /ws/preview {' \ + ' proxy_pass http://127.0.0.1:3002;' \ + ' proxy_http_version 1.1;' \ + ' proxy_set_header Upgrade \$http_upgrade;' \ + ' proxy_set_header Connection \"Upgrade\";' \ + ' proxy_set_header Host \$host;' \ + ' proxy_set_header X-Real-IP \$remote_addr;' \ + ' proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;' \ + ' proxy_read_timeout 86400;' \ + '}' \ + '' \ '# WebSocket 反向代理 → 看板实时推送端口 8080' \ 'location /ws {' \ ' proxy_pass http://127.0.0.1:8080;' \ @@ -418,8 +503,8 @@ jobs: - name: 📡 推送部署记录到 Notion env: - NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} - CHANGES_DB_ID: ${{ secrets.CHANGES_DB_ID }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + CHANGES_DB_ID: ${{ secrets.NOTION_CHANGE_LOG_DB_ID }} EVENT_TYPE: commit COMMIT_SHA: ${{ github.sha }} COMMIT_MSG: "[CD 部署] ${{ github.event.head_commit.message }}" diff --git a/.github/workflows/generate-module-doc.yml b/.github/workflows/generate-module-doc.yml index 911e760f..110e4ff0 100644 --- a/.github/workflows/generate-module-doc.yml +++ b/.github/workflows/generate-module-doc.yml @@ -1,4 +1,4 @@ -name: 铸渊 · HoloLake Era 模块文档自动生成 +name: 铸渊 · 光湖纪元 模块文档自动生成 on: push: @@ -13,9 +13,14 @@ on: - 'm11-module/**' - 'm12-kanban/**' - 'm15-cloud-drive/**' + - 'm18-health-check/**' - 'dingtalk-bot/**' - 'backend-integration/**' - 'status-board/**' + - 'backend/**' + - 'frontend/**' + - 'notification/**' + - 'ticket-system/**' - 'scripts/generate-module-doc.js' workflow_dispatch: @@ -37,7 +42,7 @@ jobs: with: node-version: '20' - - name: 生成 HoloLake Era 模块文档 + - name: 生成光湖纪元模块文档 run: node scripts/generate-module-doc.js - name: 提交更新文档 @@ -48,7 +53,7 @@ jobs: if git diff --cached --quiet; then echo "📋 文档无变化,跳过提交" else - git commit -m "📋 自动更新 HoloLake Era 模块文档 · $(date -u +%Y-%m-%d\ %H:%M\ UTC)" + git commit -m "📋 自动更新光湖纪元模块文档 · $(date -u +%Y-%m-%d\ %H:%M\ UTC)" git pull --rebase origin main || echo "⚠️ rebase 失败(可能有冲突),尝试直接推送..." git push || { echo "⚠️ 推送失败,尝试重新拉取后再推送..." @@ -91,7 +96,7 @@ jobs: id: check run: | CHANGED_FILES="${{ steps.changed.outputs.files }}" - MODULES="m01-login m03-personality m05-user-center m06-ticket m07-dialogue-ui m10-cloud m11-module m12-kanban m15-cloud-drive dingtalk-bot backend-integration status-board" + MODULES="m01-login m03-personality m05-user-center m06-ticket m07-dialogue-ui m10-cloud m11-module m12-kanban m15-cloud-drive m18-health-check dingtalk-bot backend-integration status-board backend frontend notification ticket-system" RESULT="pass" DETAILS="" diff --git a/.github/workflows/notion-callback-pipeline.yml b/.github/workflows/notion-callback-pipeline.yml new file mode 100644 index 00000000..463d7ced --- /dev/null +++ b/.github/workflows/notion-callback-pipeline.yml @@ -0,0 +1,317 @@ +name: Notion Callback Pipeline +# 📥 Notion → GitHub 回传闭环管道 +# +# 当 Notion 侧核心大脑人格体处理完 SYSLOG 工单后, +# 通过 repository_dispatch 回传处理结果到 GitHub。 +# 铸渊 Agent 接收回传,重新唤醒核心大脑认知进行二次核验, +# 确认新广播生成无误后,自动发送邮件通知开发者。 +# +# 触发方式: +# Notion 侧 Agent 调用 GitHub API 发送 repository_dispatch 事件 +# event_type: notion-syslog-callback +# client_payload: { +# broadcast_id: "BC-XXX-XXX-XX", +# dev_email: "developer@email.com", +# notion_result: "Notion 侧处理结果文本", +# ticket_id: "Notion 工单 ID", +# status: "approved" | "needs_revision" +# } +# +# 闭环流程: +# ① Notion 侧处理完成 → repository_dispatch 回传 +# ② 铸渊 Agent 接收并解析回传数据 +# ③ 重新唤醒铸渊核心大脑认知(二次核验) +# ④ 核心大脑核对广播生成无误 +# ⑤ 铸渊 Agent 发送邮件通知开发者 +# ⑥ 闭环结束 +# +# 依赖 Secrets: +# LLM_API_KEY 第三方 LLM 平台密钥 +# LLM_BASE_URL 第三方 LLM 平台 API 地址 +# NOTION_API_TOKEN Notion API token +# CORE_BRAIN_PAGE_ID 曜冥核心大脑 v4.0 页面 ID +# PORTRAIT_DB_ID 开发者动态画像库数据库 ID +# FINGERPRINT_DB_ID 模块指纹注册表数据库 ID +# SMTP_USER QQ 邮箱地址 +# SMTP_PASS QQ 邮箱 SMTP 授权码 + +on: + repository_dispatch: + types: [notion-syslog-callback] + +jobs: + verify-and-notify: + name: 📥 Notion 回传 · 二次核验 · 邮件通知 + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: 📥 Parse Notion callback + id: callback + uses: actions/github-script@v7 + with: + script: | + const payload = context.payload.client_payload || {}; + + const broadcastId = payload.broadcast_id || ''; + const devEmail = payload.dev_email || ''; + const notionResult = payload.notion_result || ''; + const ticketId = payload.ticket_id || ''; + const status = payload.status || 'unknown'; + + if (!broadcastId) { + core.setFailed('❌ 缺少 broadcast_id'); + return; + } + + core.setOutput('broadcast_id', broadcastId); + core.setOutput('dev_email', devEmail); + core.setOutput('notion_result', notionResult); + core.setOutput('ticket_id', ticketId); + core.setOutput('status', status); + + console.log(`📥 Notion 回传: 广播=${broadcastId}, 状态=${status}, 工单=${ticketId}`); + + - name: 🔍 铸渊 Agent · 模块二次验证 + id: verify + env: + SYSLOG_CONTENT: ${{ steps.callback.outputs.notion_result }} + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + AUTHOR: notion-callback + run: node scripts/verify-modules.js + + - name: 🧠 二次核验 · 唤醒铸渊核心大脑 + id: persona + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + SUBMIT_TYPE: syslog + SUBMIT_CONTENT: ${{ steps.callback.outputs.notion_result }} + AUTHOR: notion-callback + MODULE_VERIFY_RESULT: ${{ steps.verify.outputs.verify_report }} + NOTION_CALLBACK_RESULT: ${{ steps.callback.outputs.notion_result }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }} + PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }} + FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }} + run: node scripts/wake-persona.js + + - name: 📡 推送广播到 GitHub(Phase B4) + if: steps.persona.outcome == 'success' && steps.callback.outputs.status == 'approved' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + DEVELOPER_NAME: notion-callback + BROADCAST_CONTENT: ${{ steps.persona.outputs.result }} + run: node scripts/push-broadcast-to-github.js + + - name: 📝 回写 Notion 工单状态 + if: steps.callback.outputs.ticket_id + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + TICKET_ID: ${{ steps.callback.outputs.ticket_id }} + CALLBACK_STATUS: ${{ steps.callback.outputs.status }} + run: | + node -e " + var https = require('https'); + var token = process.env.NOTION_TOKEN || ''; + var ticketId = process.env.TICKET_ID || ''; + var status = process.env.CALLBACK_STATUS || 'unknown'; + + if (!token || !ticketId) { + console.log('⚠️ 跳过工单状态更新'); + process.exit(0); + } + + var notionStatus = status === 'approved' ? '✅ 已完成' : '⚠️ 异常·等人工介入'; + var body = JSON.stringify({ + properties: { + '状态': { select: { name: notionStatus } } + } + }); + + var opts = { + hostname: 'api.notion.com', + port: 443, + path: '/v1/pages/' + ticketId, + method: 'PATCH', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + 'Content-Length': Buffer.byteLength(body) + } + }; + + var req = https.request(opts, function(res) { + var data = ''; + res.on('data', function(c) { data += c; }); + res.on('end', function() { + if (res.statusCode >= 200 && res.statusCode < 300) { + console.log('✅ 工单状态已更新为 ' + notionStatus); + } else { + console.log('⚠️ 工单更新失败: ' + res.statusCode); + } + }); + }); + req.on('error', function(e) { console.log('⚠️ ' + e.message); }); + req.write(body); + req.end(); + " + + - name: 📧 发送最终确认邮件 + env: + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASS: ${{ secrets.SMTP_PASS }} + EMAIL_TO: ${{ steps.callback.outputs.dev_email }} + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + NOTION_STATUS: ${{ steps.callback.outputs.status }} + PERSONA_RESULT: ${{ steps.persona.outputs.result }} + run: | + node -e " + const nodemailer = require('nodemailer'); + + const user = process.env.SMTP_USER || ''; + const pass = process.env.SMTP_PASS || ''; + const to = process.env.EMAIL_TO || ''; + const broadcastId = process.env.BROADCAST_ID || ''; + const notionStatus = process.env.NOTION_STATUS || 'unknown'; + const result = process.env.PERSONA_RESULT || '(处理中)'; + + if (!user || !pass) { + console.log('⚠️ SMTP not configured, skipping email'); + process.exit(0); + } + if (!to) { + console.log('⚠️ No recipient email, skipping'); + process.exit(0); + } + + const isApproved = notionStatus === 'approved'; + const subjectText = isApproved + ? '[光湖系统] ' + broadcastId + ' · ✅ 新广播已生成(已核验)' + : '[光湖系统] ' + broadcastId + ' · ⚠️ 广播需修订'; + + const statusBadge = isApproved + ? '✅ 已核验通过' + : '⚠️ 需修订'; + + const resultHtml = result + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + + const html = [ + '
', + '
', + '

🌊 光湖系统 · Notion 回传核验通知

', + '

HoloLake · 铸渊核心大脑二次核验完成

', + '
', + '
', + '

📡 ' + broadcastId + '

', + '

' + statusBadge + '

', + '
' + resultHtml + '
', + '
', + '
', + '↩ 回到仓库继续开发', + '

🌀 铸渊 · 代码守护人格体 · Notion↔GitHub 闭环确认

', + '
', + '
' + ].join('\n'); + + const transporter = nodemailer.createTransport({ + host: 'smtp.qq.com', + port: 465, + secure: true, + auth: { user, pass } + }); + + transporter.sendMail({ + from: '\"光湖系统\" <' + user + '>', + to: to, + subject: subjectText, + html: html + }).then((info) => { + console.log('✅ 最终确认邮件已发送: ' + info.messageId); + }).catch((err) => { + console.log('⚠️ 邮件发送失败: ' + err.message); + }); + " + + - name: 📝 更新大脑记忆 + run: | + node -e " + const fs = require('fs'); + const path = require('path'); + + const memoryPath = path.join('.github', 'brain', 'memory.json'); + let memory = {}; + try { + memory = JSON.parse(fs.readFileSync(memoryPath, 'utf8')); + } catch (_) { + memory = { events: [] }; + } + if (!memory.events) memory.events = []; + + const broadcastId = process.env.BROADCAST_ID || 'UNKNOWN'; + const status = process.env.NOTION_STATUS || 'unknown'; + const now = new Date().toISOString(); + + // 检查是否已存在相同事件(按 broadcast_id 精确匹配 + 同类型 + 同日期) + const eventKey = 'Notion 回传核验 · ' + broadcastId; + const todayStr = now.slice(0, 10); + const existingIdx = memory.events.findIndex(function(e) { + return e.type === 'notion_callback' && + e.broadcast_id === broadcastId && + e.date === todayStr; + }); + + const newEvent = { + date: todayStr, + type: 'notion_callback', + broadcast_id: broadcastId, + description: eventKey + ' · 状态: ' + status, + by: '铸渊Agent·Notion回传管道' + }; + + if (existingIdx >= 0) { + // 同一 broadcast_id 只保留最新的一条 + memory.events[existingIdx] = newEvent; + console.log('ℹ️ 已更新同日同广播事件'); + } else { + memory.events.unshift(newEvent); + } + + // 保留最近 20 条事件 + if (memory.events.length > 20) { + memory.events = memory.events.slice(0, 20); + } + + fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2) + '\n'); + console.log('✅ 大脑记忆已更新'); + " + env: + BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }} + NOTION_STATUS: ${{ steps.callback.outputs.status }} + + - name: 📤 Commit memory update + run: | + git config user.name "zhuyuan-agent[bot]" + git config user.email "zhuyuan-agent[bot]@users.noreply.github.com" + git add .github/brain/memory.json + git diff --cached --quiet || git commit -m "🧠 Notion回传核验 · ${{ steps.callback.outputs.broadcast_id }}" + git push || echo "⚠️ Push skipped (no changes or conflict)" diff --git a/.github/workflows/notion-connectivity-test.yml b/.github/workflows/notion-connectivity-test.yml index 03190d1e..20d06582 100644 --- a/.github/workflows/notion-connectivity-test.yml +++ b/.github/workflows/notion-connectivity-test.yml @@ -37,10 +37,10 @@ jobs: - name: 🧪 执行连通性测试 id: test env: - NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} SIGNAL_LOG_DB_ID: ${{ vars.SIGNAL_LOG_DB_ID }} - CHANGES_DB_ID: ${{ secrets.CHANGES_DB_ID }} - SYSLOG_DB_ID: ${{ secrets.SYSLOG_DB_ID }} + CHANGES_DB_ID: ${{ secrets.NOTION_CHANGE_LOG_DB_ID }} + SYSLOG_DB_ID: ${{ secrets.NOTION_SYSLOG_DB_ID }} run: node scripts/notion-connectivity-test.js - name: 提交信号日志 diff --git a/.github/workflows/notion-heartbeat.yml b/.github/workflows/notion-heartbeat.yml new file mode 100644 index 00000000..76653dc0 --- /dev/null +++ b/.github/workflows/notion-heartbeat.yml @@ -0,0 +1,47 @@ +name: Notion Heartbeat Monitor +# 💓 Phase B3 · 工单心跳监控 +# +# 每 5 分钟检测 Notion 工单队列中的待处理工单: +# - 超过 5 分钟未回执 → 重新触发人格体唤醒(最多 3 次) +# - 3 次重试仍无回执 → 标记工单异常 + 发邮件通知冰朔 +# +# 依赖 Secrets: +# NOTION_API_TOKEN Notion API token +# NOTION_TICKET_DB_ID 工单队列数据库 ID +# SMTP_USER 邮件发送者 +# SMTP_PASS 邮件授权码 + +on: + schedule: + - cron: '*/5 * * * *' + workflow_dispatch: + +jobs: + heartbeat: + name: 💓 工单心跳检测 + runs-on: ubuntu-latest + permissions: + contents: read + actions: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: 💓 执行心跳检测 + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASS: ${{ secrets.SMTP_PASS }} + ALERT_EMAIL: '565183519@qq.com' + run: node scripts/notion-heartbeat.js diff --git a/.github/workflows/persona-invoke.yml b/.github/workflows/persona-invoke.yml new file mode 100644 index 00000000..e2ab5d45 --- /dev/null +++ b/.github/workflows/persona-invoke.yml @@ -0,0 +1,248 @@ +name: Persona Invoke Endpoint +# 🔗 Phase B2 · Notion Agent → 铸渊人格体唤醒 +# +# Notion Agent 通过 workflow_dispatch 触发此工作流, +# 等效于 POST /api/persona/invoke 接口。 +# +# 完整流程: +# ① Notion Agent 发送 workflow_dispatch(传入工单信息) +# ② 铸渊 Agent 从 Notion 读取工单内容 +# ③ 唤醒人格体(Claude API)处理 SYSLOG +# ④ 处理结果写回 Notion 工单(receipt_status = completed) +# ⑤ 广播文件推送到 GitHub 仓库 broadcasts/{dev_id}/ +# +# 依赖 Secrets: +# LLM_API_KEY 第三方 LLM 平台密钥 +# LLM_BASE_URL 第三方 LLM 平台 API 地址 +# NOTION_API_TOKEN Notion API token +# NOTION_TICKET_DB_ID 工单队列数据库 ID +# CORE_BRAIN_PAGE_ID 曜冥核心大脑 v4.0 页面 ID +# PORTRAIT_DB_ID 开发者动态画像库 ID +# FINGERPRINT_DB_ID 模块指纹注册表 ID +# INVOKE_API_KEY 调用鉴权密钥 + +on: + workflow_dispatch: + inputs: + work_order_id: + description: 'Notion 工单页面 ID' + required: true + type: string + task_id: + description: '广播编号(如 BC-M23-001-AW)' + required: true + type: string + developer: + description: '开发者信息(如 DEV-012 Awen)' + required: false + default: 'unknown' + type: string + syslog_raw: + description: 'SYSLOG JSON 原文(可选,如为空则从 Notion 读取)' + required: false + default: '' + type: string + action: + description: '动作类型' + required: false + default: 'process_syslog' + type: string + +jobs: + invoke: + name: 🔗 唤醒人格体处理 SYSLOG + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: 🔍 模块验证 + id: verify + env: + SYSLOG_CONTENT: ${{ inputs.syslog_raw }} + BROADCAST_ID: ${{ inputs.task_id }} + AUTHOR: ${{ inputs.developer }} + run: node scripts/verify-modules.js + + - name: 🧠 唤醒人格体 + id: persona + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + BROADCAST_ID: ${{ inputs.task_id }} + SUBMIT_TYPE: syslog + SUBMIT_CONTENT: ${{ inputs.syslog_raw }} + AUTHOR: ${{ inputs.developer }} + MODULE_VERIFY_RESULT: ${{ steps.verify.outputs.verify_report }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }} + PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }} + FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }} + run: node scripts/wake-persona.js + + - name: 📝 回写 Notion 工单 + if: always() + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + WORK_ORDER_ID: ${{ inputs.work_order_id }} + PERSONA_RESULT: ${{ steps.persona.outputs.result }} + TASK_ID: ${{ inputs.task_id }} + run: | + node -e " + var https = require('https'); + + var token = process.env.NOTION_TOKEN || ''; + var workOrderId = process.env.WORK_ORDER_ID || ''; + var result = process.env.PERSONA_RESULT || '(no result)'; + var taskId = process.env.TASK_ID || ''; + + if (!token || !workOrderId) { + console.log('⚠️ 缺少 Notion 配置,跳过回写'); + process.exit(0); + } + + // 更新工单状态为 ✅ 已完成 + var body = JSON.stringify({ + properties: { + '状态': { select: { name: '✅ 已完成' } } + } + }); + + var opts = { + hostname: 'api.notion.com', + port: 443, + path: '/v1/pages/' + workOrderId, + method: 'PATCH', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + 'Content-Length': Buffer.byteLength(body) + } + }; + + var req = https.request(opts, function(res) { + var data = ''; + res.on('data', function(c) { data += c; }); + res.on('end', function() { + if (res.statusCode >= 200 && res.statusCode < 300) { + console.log('✅ 工单状态已更新为 ✅ 已完成'); + } else { + console.log('⚠️ 工单状态更新失败: ' + res.statusCode); + } + }); + }); + req.on('error', function(e) { console.log('⚠️ ' + e.message); }); + req.write(body); + req.end(); + + // 追加处理结果到工单 + var appendBody = JSON.stringify({ + children: [{ + object: 'block', + type: 'heading_2', + heading_2: { + rich_text: [{ type: 'text', text: { content: '🧠 人格体处理结果 · receipt_status: completed' } }] + } + }, { + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: [{ type: 'text', text: { content: result.slice(0, 2000) } }] + } + }] + }); + + var appendOpts = { + hostname: 'api.notion.com', + port: 443, + path: '/v1/blocks/' + workOrderId + '/children', + method: 'PATCH', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + 'Content-Length': Buffer.byteLength(appendBody) + } + }; + + var req2 = https.request(appendOpts, function(res) { + var data = ''; + res.on('data', function(c) { data += c; }); + res.on('end', function() { + if (res.statusCode >= 200 && res.statusCode < 300) { + console.log('✅ 处理结果已追加到工单'); + } else { + console.log('⚠️ 结果追加失败: ' + res.statusCode); + } + }); + }); + req2.on('error', function(e) { console.log('⚠️ ' + e.message); }); + req2.write(appendBody); + req2.end(); + " + + - name: 📡 推送广播到 GitHub + if: steps.persona.outcome == 'success' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BROADCAST_ID: ${{ inputs.task_id }} + DEVELOPER_ID: '' + DEVELOPER_NAME: ${{ inputs.developer }} + BROADCAST_CONTENT: ${{ steps.persona.outputs.result }} + run: node scripts/push-broadcast-to-github.js + + - name: 🔴 失败处理 + if: failure() + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + WORK_ORDER_ID: ${{ inputs.work_order_id }} + run: | + node -e " + var https = require('https'); + var token = process.env.NOTION_TOKEN || ''; + var workOrderId = process.env.WORK_ORDER_ID || ''; + + if (!token || !workOrderId) process.exit(0); + + var body = JSON.stringify({ + properties: { + '状态': { select: { name: '⚠️ 异常·等人工介入' } } + } + }); + + var opts = { + hostname: 'api.notion.com', + port: 443, + path: '/v1/pages/' + workOrderId, + method: 'PATCH', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + 'Content-Length': Buffer.byteLength(body) + } + }; + + var req = https.request(opts, function(res) { + var data = ''; + res.on('data', function(c) { data += c; }); + res.on('end', function() { + console.log('工单状态已标记为 ⚠️ 异常'); + }); + }); + req.on('error', function(e) { console.log(e.message); }); + req.write(body); + req.end(); + " diff --git a/.github/workflows/pm2-server-diagnose.yml b/.github/workflows/pm2-server-diagnose.yml new file mode 100644 index 00000000..642a594d --- /dev/null +++ b/.github/workflows/pm2-server-diagnose.yml @@ -0,0 +1,316 @@ +name: "🔧 铸渊 · PM2 服务诊断与健康检查" + +# ━━━ PM2 服务诊断 + 全系统健康检查 + Notion 报告 ━━━ +# 手动触发:对服务器 PM2 进程进行全面排查, +# 自动处理 errored 进程,并将诊断报告写入 Notion 工单队列。 +# +# 必需 GitHub Secrets: +# DEPLOY_HOST — 服务器 IP +# DEPLOY_USER — SSH 用户名 +# DEPLOY_KEY — SSH 私钥(完整 PEM 内容) +# DEPLOY_PATH — 网站根目录(如 /var/www/guanghulab) +# +# 可选 Secrets(有则自动写 Notion 工单): +# NOTION_API_TOKEN — Notion 集成 token +# NOTION_TICKET_DB_ID — 工单队列数据库 ID(默认 84edd0640bf146a9a5a7840107013e8c) + +on: + workflow_dispatch: + inputs: + auto_cleanup: + description: '是否自动清理 errored 进程(删除启动路径不存在的进程)' + required: false + default: 'true' + type: choice + options: + - 'true' + - 'false' + +permissions: + contents: read + +jobs: + diagnose: + name: "🔍 PM2 诊断 + 系统健康检查" + runs-on: ubuntu-latest + + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 🟢 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: 🔑 配置 SSH 连接 + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null || { + echo "⚠️ ssh-keyscan 失败,使用 StrictHostKeyChecking=no 后备" + } + + # ━━━ 阶段一:PM2 服务诊断 ━━━ + - name: "🔧 阶段一 · PM2 errored 服务排查" + id: pm2_diagnose + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " 阶段一 · PM2 errored 服务排查" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + PM2_REPORT=$(ssh -i ~/.ssh/deploy_key \ + -o StrictHostKeyChecking=no \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" ' + echo "══════════════════════════════════════" + echo " PM2 进程状态概览" + echo "══════════════════════════════════════" + pm2 status 2>/dev/null || echo "❌ pm2 未安装或不可用" + + echo "" + echo "══════════════════════════════════════" + echo " errored 进程详细日志" + echo "══════════════════════════════════════" + + # 遍历所有 errored 进程 + ERRORED_LIST=$(pm2 jlist 2>/dev/null | python3 -c " + import sys, json + try: + procs = json.load(sys.stdin) + for p in procs: + if p.get(\"pm2_env\", {}).get(\"status\") == \"errored\": + name = p.get(\"name\", \"unknown\") + pm_id = p.get(\"pm_id\", \"?\") + script = p.get(\"pm2_env\", {}).get(\"pm_exec_path\", \"unknown\") + restarts = p.get(\"pm2_env\", {}).get(\"restart_time\", 0) + print(f\"{pm_id}|{name}|{script}|{restarts}\") + except: + pass + " 2>/dev/null) + + if [ -z "$ERRORED_LIST" ]; then + echo "✅ 没有 errored 进程" + else + echo "$ERRORED_LIST" | while IFS="|" read -r pm_id name script restarts; do + echo "" + echo "━━━ 进程: $name (PM2 ID: $pm_id) ━━━" + echo " 启动脚本: $script" + echo " 重启次数: $restarts" + + # 检查脚本文件是否存在 + if [ -f "$script" ]; then + echo " 脚本文件: ✅ 存在" + else + echo " 脚本文件: ❌ 不存在" + fi + + # 查看最近日志 + echo " 最近错误日志 (最后 20 行):" + pm2 logs "$name" --lines 20 --nostream --err 2>/dev/null || echo " (无法读取日志)" + echo "" + done + fi + + echo "" + echo "══════════════════════════════════════" + echo " 所有进程详细信息" + echo "══════════════════════════════════════" + pm2 jlist 2>/dev/null | python3 -c " + import sys, json + try: + procs = json.load(sys.stdin) + for p in procs: + env = p.get(\"pm2_env\", {}) + print(f\" [{p.get(\"pm_id\")}] {p.get(\"name\")} | status={env.get(\"status\")} | script={env.get(\"pm_exec_path\",\"?\")} | restarts={env.get(\"restart_time\",0)} | memory={p.get(\"monit\",{}).get(\"memory\",0)}\") + except Exception as e: + print(f\" 解析失败: {e}\") + " 2>/dev/null + + echo "" + echo "══════════════════════════════════════" + echo " 端口占用情况" + echo "══════════════════════════════════════" + netstat -tlnp 2>/dev/null | grep -E "3000|3001|3002|3721|8080" || \ + ss -tlnp 2>/dev/null | grep -E "3000|3001|3002|3721|8080" || \ + echo " (netstat/ss 不可用)" + ') + + echo "$PM2_REPORT" + + # 保存到文件供后续步骤使用 + echo "$PM2_REPORT" > /tmp/pm2-report.txt + + # 设置输出 + { + echo "pm2_report<> "$GITHUB_OUTPUT" + + # ━━━ 阶段一 · 自动清理 ━━━ + - name: "🧹 阶段一 · 自动清理 errored 进程" + if: ${{ inputs.auto_cleanup == 'true' }} + id: pm2_cleanup + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " 自动清理 errored 进程" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + CLEANUP_REPORT=$(ssh -i ~/.ssh/deploy_key \ + -o StrictHostKeyChecking=no \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" ' + ERRORED_LIST=$(pm2 jlist 2>/dev/null | python3 -c " + import sys, json + try: + procs = json.load(sys.stdin) + for p in procs: + if p.get(\"pm2_env\", {}).get(\"status\") == \"errored\": + name = p.get(\"name\", \"unknown\") + script = p.get(\"pm2_env\", {}).get(\"pm_exec_path\", \"unknown\") + print(f\"{name}|{script}\") + except: + pass + " 2>/dev/null) + + CLEANED="" + SKIPPED="" + + if [ -z "$ERRORED_LIST" ]; then + echo "✅ 没有需要清理的 errored 进程" + else + echo "$ERRORED_LIST" | while IFS="|" read -r name script; do + if [ ! -f "$script" ]; then + echo "🗑️ 删除 $name(脚本 $script 不存在)" + pm2 delete "$name" 2>/dev/null && echo " ✅ 已删除" || echo " ❌ 删除失败" + else + echo "⚠️ 保留 $name(脚本 $script 存在,可能是运行时错误)" + echo " 尝试重启..." + pm2 restart "$name" 2>/dev/null && echo " ✅ 重启成功" || echo " ❌ 重启失败" + fi + done + + echo "" + echo "📊 清理后 PM2 状态:" + pm2 status 2>/dev/null + pm2 save 2>/dev/null || echo "⚠️ pm2 save 失败" + fi + ') + + echo "$CLEANUP_REPORT" + echo "$CLEANUP_REPORT" > /tmp/cleanup-report.txt + + { + echo "cleanup_report<> "$GITHUB_OUTPUT" + + # ━━━ 阶段二:全系统健康检查 ━━━ + - name: "🏥 阶段二 · 全系统健康检查" + id: health_check + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " 阶段二 · 全系统健康检查" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + HEALTH_REPORT=$(ssh -i ~/.ssh/deploy_key \ + -o StrictHostKeyChecking=no \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" ' + echo "══════════════════════════════════════" + echo " Nginx 状态" + echo "══════════════════════════════════════" + nginx -t 2>&1 || echo "❌ Nginx 配置检测失败" + systemctl status nginx --no-pager -l 2>/dev/null | head -15 || echo "⚠️ systemctl 不可用" + + echo "" + echo "══════════════════════════════════════" + echo " SSL 证书有效期" + echo "══════════════════════════════════════" + if [ -f "/etc/letsencrypt/live/guanghulab.com/fullchain.pem" ]; then + openssl x509 -in /etc/letsencrypt/live/guanghulab.com/fullchain.pem -noout -dates 2>/dev/null || echo "⚠️ 无法读取 SSL 证书" + else + echo "⚠️ SSL 证书文件不存在于默认路径" + # 尝试查找其他证书路径 + find /etc/letsencrypt/live/ -name "fullchain.pem" 2>/dev/null | head -3 || echo " 未找到 Let'\''s Encrypt 证书" + find /etc/nginx/ -name "*.pem" -o -name "*.crt" 2>/dev/null | head -3 || echo " 未找到 Nginx SSL 文件" + fi + + echo "" + echo "══════════════════════════════════════" + echo " 磁盘空间" + echo "══════════════════════════════════════" + df -h 2>/dev/null || echo "❌ df 命令不可用" + + echo "" + echo "══════════════════════════════════════" + echo " 内存使用" + echo "══════════════════════════════════════" + free -h 2>/dev/null || echo "❌ free 命令不可用" + + echo "" + echo "══════════════════════════════════════" + echo " PM2 最终状态" + echo "══════════════════════════════════════" + pm2 status 2>/dev/null || echo "❌ pm2 不可用" + + echo "" + echo "══════════════════════════════════════" + echo " Node.js 版本" + echo "══════════════════════════════════════" + node -v 2>/dev/null || echo "❌ Node.js 未安装" + npm -v 2>/dev/null || echo "❌ npm 未安装" + + echo "" + echo "══════════════════════════════════════" + echo " 网站可访问性检查" + echo "══════════════════════════════════════" + curl -sI https://guanghulab.com 2>/dev/null | head -5 || echo "⚠️ HTTPS 访问失败" + echo "---" + curl -s http://127.0.0.1:3000/health 2>/dev/null | head -c 200 || echo "⚠️ 端口 3000 无响应" + echo "" + echo "---" + curl -s http://127.0.0.1:3001/api/v1/ 2>/dev/null | head -c 200 || echo "⚠️ 端口 3001 无响应" + echo "" + echo "---" + curl -s http://127.0.0.1:3002/api/ps/ 2>/dev/null | head -c 200 || echo "⚠️ 端口 3002 无响应" + echo "" + + echo "" + echo "══════════════════════════════════════" + echo " 系统运行时间" + echo "══════════════════════════════════════" + uptime 2>/dev/null || echo "❌ uptime 不可用" + ') + + echo "$HEALTH_REPORT" + echo "$HEALTH_REPORT" > /tmp/health-report.txt + + { + echo "health_report<> "$GITHUB_OUTPUT" + + # ━━━ 阶段三:生成 Notion 诊断报告 ━━━ + - name: "📋 阶段三 · 写入 Notion 工单" + if: ${{ secrets.NOTION_API_TOKEN != '' }} + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID || '84edd0640bf146a9a5a7840107013e8c' }} + PM2_REPORT: ${{ steps.pm2_diagnose.outputs.pm2_report }} + CLEANUP_REPORT: ${{ steps.pm2_cleanup.outputs.cleanup_report }} + HEALTH_REPORT: ${{ steps.health_check.outputs.health_report }} + run: node scripts/server-diagnose-report.js + + - name: "📋 阶段三 · 写入 Notion 工单(环境检查)" + if: ${{ secrets.NOTION_API_TOKEN == '' }} + run: | + echo "⚠️ Notion credentials 未配置,跳过工单创建" + echo "诊断报告仅输出到 Actions 日志" + + - name: "✅ 诊断完成" + run: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " ✅ PM2 诊断完毕 · $(date '+%Y-%m-%d %H:%M:%S')" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/.github/workflows/ps-on-build.yml b/.github/workflows/ps-on-build.yml new file mode 100644 index 00000000..05ed36da --- /dev/null +++ b/.github/workflows/ps-on-build.yml @@ -0,0 +1,45 @@ +name: "🌊 Persona Studio · 代码生成" + +# 工作流存根 · "我要开发" → 代码生成+测试+打包 +# 实际代码生成由 persona-studio/backend/brain/code-generator.js 处理 +# 此工作流用于重量级构建任务的异步执行 + +on: + workflow_dispatch: + inputs: + dev_id: + description: '体验者编号(EXP-XXX)' + required: true + email: + description: '完成后通知邮箱' + required: true + +permissions: + contents: write + +jobs: + build: + name: "🚀 代码生成" + runs-on: ubuntu-latest + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 🟢 配置 Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: 📦 安装依赖 + run: | + cd persona-studio/backend + npm install --production + + - name: 🔨 执行代码生成 + env: + MODEL_API_KEY: ${{ secrets.MODEL_API_KEY }} + run: | + echo "🔨 开始代码生成" + echo " 体验者: ${{ github.event.inputs.dev_id }}" + echo " 通知邮箱: ${{ github.event.inputs.email }}" + echo "✅ 代码生成任务已排队(由后端服务处理)" diff --git a/.github/workflows/ps-on-chat.yml b/.github/workflows/ps-on-chat.yml new file mode 100644 index 00000000..e49db0f1 --- /dev/null +++ b/.github/workflows/ps-on-chat.yml @@ -0,0 +1,35 @@ +name: "🌊 Persona Studio · 对话处理" + +# 工作流存根 · 对话消息处理 +# 实际对话由 persona-studio/backend/routes/chat.js + persona-engine.js 处理 +# 此工作流用于异步任务和审计 + +on: + workflow_dispatch: + inputs: + dev_id: + description: '体验者编号(EXP-XXX)' + required: true + task_type: + description: '任务类型' + required: false + default: 'chat' + +permissions: + contents: read + +jobs: + process: + name: "💬 对话处理" + runs-on: ubuntu-latest + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 📝 记录对话事件 + run: | + echo "📝 对话事件" + echo " 体验者: ${{ github.event.inputs.dev_id }}" + echo " 任务类型: ${{ github.event.inputs.task_type }}" + echo " 时间: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "✅ 事件已记录" diff --git a/.github/workflows/ps-on-complete.yml b/.github/workflows/ps-on-complete.yml new file mode 100644 index 00000000..dc8a4ffa --- /dev/null +++ b/.github/workflows/ps-on-complete.yml @@ -0,0 +1,37 @@ +name: "🌊 Persona Studio · 完成通知" + +# 工作流存根 · 代码生成完成 → 邮件推送 +# 实际邮件发送由 persona-studio/backend/utils/email-sender.js 处理 +# 此工作流用于 workflow_run 联动 + +on: + workflow_run: + workflows: ["🌊 Persona Studio · 代码生成"] + types: [completed] + + workflow_dispatch: + inputs: + dev_id: + description: '体验者编号(EXP-XXX)' + required: true + email: + description: '通知邮箱' + required: true + +permissions: + contents: read + +jobs: + notify: + name: "📧 完成通知" + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 📧 发送通知 + run: | + echo "📧 发送完成通知" + echo " 触发方式: ${{ github.event_name }}" + echo "✅ 通知已排队(由后端服务处理)" diff --git a/.github/workflows/ps-on-login.yml b/.github/workflows/ps-on-login.yml new file mode 100644 index 00000000..e95850de --- /dev/null +++ b/.github/workflows/ps-on-login.yml @@ -0,0 +1,48 @@ +name: "🌊 Persona Studio · 登录校验" + +# 工作流存根 · 合作者登录时触发校验 +# 实际登录校验由 persona-studio/backend/routes/auth.js 处理 +# 此工作流用于审计日志和扩展 + +on: + workflow_dispatch: + inputs: + dev_id: + description: '体验者编号(EXP-XXX)' + required: true + +permissions: + contents: read + +jobs: + verify: + name: "🔐 校验开发编号" + runs-on: ubuntu-latest + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + + - name: 🔍 校验编号 + run: | + DEV_ID="${{ github.event.inputs.dev_id }}" + echo "校验编号: $DEV_ID" + + if ! echo "$DEV_ID" | grep -qE '^EXP-[0-9]{3,}$'; then + echo "❌ 编号格式不正确" + exit 1 + fi + + if node -e " + const r = require('./persona-studio/brain/registry.json'); + const d = r.developers['$DEV_ID']; + if (!d) { console.log('❌ 编号未注册'); process.exit(1); } + if (d.status !== 'active' && d.status !== 'pending_activation') { + console.log('❌ 编号未激活:', d.status); process.exit(1); + } + console.log('✅ 校验通过:', d.name || DEV_ID); + "; then + echo "✅ 登录校验通过" + else + echo "❌ 登录校验失败" + exit 1 + fi diff --git a/.github/workflows/push-broadcast.yml b/.github/workflows/push-broadcast.yml new file mode 100644 index 00000000..4d0dbafa --- /dev/null +++ b/.github/workflows/push-broadcast.yml @@ -0,0 +1,63 @@ +name: 铸渊 · Push Broadcast · Notion → 飞书文档B + +# 链路2:新广播推送 +# Notion 新广播页面 → 追加到飞书文档B顶部 +# +# 依赖 Secrets: +# NOTION_TOKEN Notion API token +# FEISHU_APP_ID 飞书应用 App ID +# FEISHU_APP_SECRET 飞书应用 App Secret +# FEISHU_DOC_B_ID 飞书文档B的 document_id +# +# dispatch payload: +# broadcast_page_id Notion 广播页面 ID + +on: + repository_dispatch: + types: [push-broadcast] + workflow_dispatch: + inputs: + broadcast_page_id: + description: 'Notion 广播页面 ID' + required: true + +jobs: + push-broadcast: + name: 📡 推送广播到飞书 + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 📡 推送广播 → 飞书文档B + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + BROADCAST_PAGE_ID: ${{ github.event.client_payload.broadcast_page_id || github.event.inputs.broadcast_page_id }} + FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }} + FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} + FEISHU_DOC_B_ID: ${{ secrets.FEISHU_DOC_B_ID }} + run: node scripts/push-broadcast.js + + - name: 📢 通知飞书群有新广播 + if: success() + env: + FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }} + FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} + ALERT_CHAT_ID: ${{ secrets.FEISHU_ALERT_CHAT_ID }} + run: node scripts/send-feishu-alert.js "push-broadcast" "📡 新广播已推送到飞书文档B,请查看飞书广播收件箱。" || true + + - name: 🔴 失败告警 → 飞书通知 + if: failure() + env: + FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }} + FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} + ALERT_CHAT_ID: ${{ secrets.FEISHU_ALERT_CHAT_ID }} + run: node scripts/send-feishu-alert.js "push-broadcast" diff --git a/.github/workflows/receive-syslog.yml b/.github/workflows/receive-syslog.yml new file mode 100644 index 00000000..98805d94 --- /dev/null +++ b/.github/workflows/receive-syslog.yml @@ -0,0 +1,61 @@ +name: 铸渊 · Receive SYSLOG · 飞书机器人 → GitHub → Notion + +# 链路3:SYSLOG 回传 +# 飞书机器人发来的 SYSLOG → 存入仓库 + 创建 Notion 条目 + 创建霜砚工单 +# +# 依赖 Secrets: +# NOTION_TOKEN Notion API token +# NOTION_SYSLOG_DB_ID SYSLOG 收件箱数据库 ID +# NOTION_TICKET_DB_ID 霜砚工单数据库 ID +# +# dispatch payload: +# syslog SYSLOG JSON 全文 + +on: + repository_dispatch: + types: [receive-syslog] + +jobs: + receive-syslog: + name: 📥 接收 SYSLOG 回传 + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 📥 处理 SYSLOG + env: + SYSLOG_JSON: ${{ toJSON(github.event.client_payload.syslog) }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + NOTION_SYSLOG_DB_ID: ${{ secrets.NOTION_SYSLOG_DB_ID }} + NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID }} + run: node scripts/receive-syslog.js + + - name: 💾 提交 SYSLOG 文件到仓库 + run: | + git config user.name "zhuyuan-bot" + git config user.email "zhuyuan-bot@guanghulab.com" + git add syslog/ + if git diff --cached --quiet; then + echo "No new syslog files to commit" + else + git pull --rebase origin main || true + git commit -m "📥 铸渊接收 SYSLOG 回传 [skip ci]" + git push origin main + fi + + - name: 🔴 失败告警 → 飞书通知 + if: failure() + env: + FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }} + FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} + ALERT_CHAT_ID: ${{ secrets.FEISHU_ALERT_CHAT_ID }} + run: node scripts/send-feishu-alert.js "receive-syslog" diff --git a/.github/workflows/sync-login-entry.yml b/.github/workflows/sync-login-entry.yml new file mode 100644 index 00000000..580f0005 --- /dev/null +++ b/.github/workflows/sync-login-entry.yml @@ -0,0 +1,49 @@ +name: 铸渊 · Sync Login Entry · Notion → 飞书文档A + +# 链路1:登录入口同步 +# Notion 霜砚登录入口页面 → 全量替换飞书文档A +# +# 依赖 Secrets: +# NOTION_TOKEN Notion API token +# NOTION_LOGIN_PAGE_ID Notion 登录入口页面 ID +# FEISHU_APP_ID 飞书应用 App ID +# FEISHU_APP_SECRET 飞书应用 App Secret +# FEISHU_DOC_A_ID 飞书文档A的 document_id + +on: + repository_dispatch: + types: [sync-login-entry] + workflow_dispatch: + +jobs: + sync-login-entry: + name: 📋 同步登录入口到飞书 + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 🔗 同步 Notion 登录入口 → 飞书文档A + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + NOTION_LOGIN_PAGE_ID: ${{ secrets.NOTION_LOGIN_PAGE_ID }} + FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }} + FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} + FEISHU_DOC_A_ID: ${{ secrets.FEISHU_DOC_A_ID }} + run: node scripts/sync-login-entry.js + + - name: 🔴 失败告警 → 飞书通知 + if: failure() + env: + FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }} + FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} + ALERT_CHAT_ID: ${{ secrets.FEISHU_ALERT_CHAT_ID }} + run: node scripts/send-feishu-alert.js "sync-login-entry" diff --git a/.github/workflows/sync-persona-studio.yml b/.github/workflows/sync-persona-studio.yml new file mode 100644 index 00000000..be664b8d --- /dev/null +++ b/.github/workflows/sync-persona-studio.yml @@ -0,0 +1,58 @@ +name: 🔄 铸渊跨仓库同步 · persona-studio + +on: + push: + branches: [main] + paths: + - 'persona-studio/brain/**' + - 'persona-studio/backend/**' + - 'persona-studio/frontend/**' + workflow_dispatch: + inputs: + sync_target: + description: '同步目标(all / brain / frontend / backend)' + required: false + default: 'all' + type: choice + options: + - all + - brain + - frontend + - backend + +concurrency: + group: sync-persona-studio + cancel-in-progress: true + +jobs: + sync: + name: 同步到 persona-studio 仓库 + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout guanghulab + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 执行跨仓库同步 + env: + CROSS_REPO_TOKEN: ${{ secrets.CROSS_REPO_TOKEN }} + SYNC_TARGET: ${{ github.event.inputs.sync_target || 'all' }} + run: node scripts/cross-repo-sync.js + + - name: 通知 persona-studio 仓库 + if: env.CROSS_REPO_TOKEN != '' + env: + CROSS_REPO_TOKEN: ${{ secrets.CROSS_REPO_TOKEN }} + run: | + curl -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $CROSS_REPO_TOKEN" \ + https://api.github.com/repos/qinfendebingshuo/persona-studio/dispatches \ + -d '{"event_type":"brain-sync","client_payload":{"source":"guanghulab","timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}}' \ + 2>/dev/null || echo "⚠️ 跨仓库通知未发送(需要 CROSS_REPO_TOKEN secret)" diff --git a/.github/workflows/syslog-auto-pipeline.yml b/.github/workflows/syslog-auto-pipeline.yml new file mode 100644 index 00000000..a0d84b10 --- /dev/null +++ b/.github/workflows/syslog-auto-pipeline.yml @@ -0,0 +1,343 @@ +name: SYSLOG Auto Pipeline +# 📡 SYSLOG 自助提交系统 · 全自动闭环 +# +# 开发者在 GitHub Discussion 提交 SYSLOG 或提问 +# → Actions 自动解析 → 模块上传验证 → 调用 LLM API 唤醒人格体 +# → 模块闭环测试 → 写入 Notion 工单 → 发邮件给开发者 → Discussion 回复 +# +# 闭环流程: +# ① 开发者提交 SYSLOG → Discussion 触发 +# ② 铸渊 Agent 解析提交内容 +# ③ 铸渊 Agent 检测模块是否上传到仓库 +# ④ 唤醒铸渊核心大脑认知(注入模块验证结果) +# ⑤ 核心大脑处理 SYSLOG + 生成广播 +# ⑥ 铸渊 Agent 推送 Notion 工单(触发 Notion 侧处理) +# ⑦ 邮件通知开发者 +# ⑧ Discussion 回复闭环 +# +# 依赖 Secrets: +# LLM_API_KEY 第三方 LLM 平台密钥(必须) +# LLM_BASE_URL 第三方 LLM 平台 API 地址(必须,如 https://api.xxx.com/v1) +# NOTION_API_TOKEN Notion API token +# NOTION_TICKET_DB_ID 霜砚工单数据库 ID +# CORE_BRAIN_PAGE_ID 曜冥核心大脑 v4.0 页面 ID(v4.0 协议动态注入) +# PORTRAIT_DB_ID 开发者动态画像库数据库 ID(v4.0 协议动态注入) +# FINGERPRINT_DB_ID 模块指纹注册表数据库 ID(v4.0 协议动态注入) +# SMTP_USER QQ 邮箱地址 +# SMTP_PASS QQ 邮箱 SMTP 授权码 + +on: + discussion: + types: [created] + +jobs: + process: + name: 📡 处理 SYSLOG 提交 / 广播提问 + runs-on: ubuntu-latest + if: contains(github.event.discussion.category.name, 'SYSLOG') + permissions: + contents: write + discussions: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: 🔍 Parse submission + id: parse + uses: actions/github-script@v7 + with: + script: | + const body = context.payload.discussion.body || ''; + const title = context.payload.discussion.title || ''; + + // Parse structured fields from discussion body + function extractField(text, label) { + // Match form field format: ### Label\n\nValue + const regex = new RegExp('###\\s*' + label + '\\s*\\n+([^\\n#]+)', 'i'); + const match = text.match(regex); + return match ? match[1].trim() : ''; + } + + const broadcastId = extractField(body, '广播编号') || ''; + const submitType = extractField(body, '类型') || ''; + const email = extractField(body, '你的邮箱') || ''; + + // Extract content (everything after "### 内容") + const contentMatch = body.match(/###\s*内容\s*\n+([\s\S]*?)$/i); + const content = contentMatch ? contentMatch[1].trim() : ''; + + // Determine type from title or body + let type = 'question'; // default + if (submitType.includes('SYSLOG') || title.includes('SYSLOG')) { + type = 'syslog'; + } else if (submitType.includes('提问') || title.includes('提问')) { + type = 'question'; + } + + // Validate + if (!broadcastId) { + core.setFailed('❌ 缺少广播编号,请在提交时填写广播编号'); + return; + } + if (!email) { + core.setFailed('❌ 缺少邮箱,请在提交时填写你的邮箱'); + return; + } + if (!content) { + core.setFailed('❌ 缺少内容,请粘贴你的 SYSLOG 或问题'); + return; + } + + core.setOutput('broadcast_id', broadcastId); + core.setOutput('type', type); + core.setOutput('email', email); + core.setOutput('content', content); + core.setOutput('discussion_number', context.payload.discussion.number); + core.setOutput('author', context.payload.discussion.user.login); + + console.log(`📡 解析完成: 广播=${broadcastId}, 类型=${type}, 邮箱=${email}`); + + - name: 🔍 铸渊 Agent · 模块上传验证 + id: verify + env: + SYSLOG_CONTENT: ${{ steps.parse.outputs.content }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + AUTHOR: ${{ steps.parse.outputs.author }} + run: node scripts/verify-modules.js + + - name: 🧠 Auto-detect and wake up persona + id: persona + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + SUBMIT_TYPE: ${{ steps.parse.outputs.type }} + SUBMIT_CONTENT: ${{ steps.parse.outputs.content }} + AUTHOR: ${{ steps.parse.outputs.author }} + MODULE_VERIFY_RESULT: ${{ steps.verify.outputs.verify_report }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }} + PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }} + FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }} + run: node scripts/wake-persona.js + + - name: 📋 创建标准化 Notion 工单(Phase B1) + id: ticket + if: ${{ secrets.NOTION_API_TOKEN }} + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + SUBMIT_TYPE: ${{ steps.parse.outputs.type }} + PERSONA_RESULT: ${{ steps.persona.outputs.result }} + MODULE_VERIFY: ${{ steps.verify.outputs.verify_result }} + MODULES_UPLOADED: ${{ steps.verify.outputs.modules_uploaded }} + DEVELOPER: ${{ steps.parse.outputs.author }} + SYSLOG_RAW: ${{ steps.parse.outputs.content }} + run: node scripts/create-standardized-ticket.js + + - name: 📡 推送广播到 GitHub(Phase B4) + if: steps.persona.outcome == 'success' && steps.parse.outputs.type == 'syslog' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + DEVELOPER_NAME: ${{ steps.parse.outputs.author }} + BROADCAST_CONTENT: ${{ steps.persona.outputs.result }} + run: node scripts/push-broadcast-to-github.js + + - name: 📧 Send email to developer + env: + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASS: ${{ secrets.SMTP_PASS }} + EMAIL_TO: ${{ steps.parse.outputs.email }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + SUBMIT_TYPE: ${{ steps.parse.outputs.type }} + PERSONA_RESULT: ${{ steps.persona.outputs.result }} + run: | + node -e " + const nodemailer = require('nodemailer'); + + const user = process.env.SMTP_USER || ''; + const pass = process.env.SMTP_PASS || ''; + const to = process.env.EMAIL_TO || ''; + const broadcastId = process.env.BROADCAST_ID || ''; + const type = process.env.SUBMIT_TYPE || 'syslog'; + const result = process.env.PERSONA_RESULT || '(处理中,请稍后)'; + + if (!user || !pass) { + console.log('⚠️ SMTP not configured, skipping email'); + process.exit(0); + } + if (!to) { + console.log('⚠️ No recipient email, skipping'); + process.exit(0); + } + + const subjectText = type === 'syslog' + ? '[光湖系统] ' + broadcastId + ' · 新广播已生成' + : '[光湖系统] ' + broadcastId + ' · 问题已解答'; + + const resultHtml = result + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + + const html = [ + '
', + '
', + '

🌊 光湖系统 · 自动通知

', + '

HoloLake · 人格语言操作系统

', + '
', + '
', + '

📡 ' + broadcastId + '

', + '
' + resultHtml + '
', + '
', + '
', + '↩ 回到仓库继续开发', + '

🌀 铸渊 · 代码守护人格体 · 自动发送

', + '
', + '
' + ].join('\n'); + + const transporter = nodemailer.createTransport({ + host: 'smtp.qq.com', + port: 465, + secure: true, + auth: { user, pass } + }); + + transporter.sendMail({ + from: '\"光湖系统\" <' + user + '>', + to: to, + subject: subjectText, + html: html + }).then((info) => { + console.log('✅ 邮件已发送: ' + info.messageId); + }).catch((err) => { + console.log('⚠️ 邮件发送失败: ' + err.message); + }); + " + + - name: 💬 Reply to Discussion + uses: actions/github-script@v7 + with: + script: | + const number = ${{ steps.parse.outputs.discussion_number }}; + const type = '${{ steps.parse.outputs.type }}'; + const broadcastId = '${{ steps.parse.outputs.broadcast_id }}'; + const email = '${{ steps.parse.outputs.email }}'; + const modulesUploaded = '${{ steps.verify.outputs.modules_uploaded }}' === 'true'; + const moduleCount = '${{ steps.verify.outputs.module_count }}' || '0'; + + const typeLabel = type === 'syslog' ? 'SYSLOG 闭环处理' : '问题解答'; + const maskedEmail = email.length > 4 + ? email.replace(/(.{2})(.*)(@.*)/, '$1***$3') + : '***'; + const moduleStatus = moduleCount === '0' + ? 'ℹ️ 未检测到模块引用' + : (modulesUploaded ? '✅ 全部已上传' : '⚠️ 部分模块未上传'); + + const body = [ + '✅ **已处理** · ' + typeLabel, + '', + '| 项目 | 内容 |', + '|------|------|', + '| 📡 广播编号 | `' + broadcastId + '` |', + '| 🔍 模块验证 | ' + moduleStatus + ' |', + '| 📧 结果发送至 | `' + maskedEmail + '` |', + '| 🤖 处理人格体 | 铸渊 |', + '| ⏰ 处理时间 | ' + new Date().toISOString() + ' |', + '', + '> 铸渊核心大脑已完成 SYSLOG 验收 + 模块检测 + 广播生成。', + '> 结果已发送到你的邮箱,Notion 侧工单已创建。', + '>', + '> 如未收到邮件,请检查垃圾箱或重新提交。' + ].join('\n'); + + // Use GraphQL to add discussion comment + const query = ` + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { + discussionId: $discussionId, + body: $body + }) { + comment { id } + } + } + `; + + try { + // Get the discussion node ID + const { repository } = await github.graphql(` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + discussion(number: $number) { + id + } + } + } + `, { + owner: context.repo.owner, + repo: context.repo.repo, + number: number + }); + + await github.graphql(query, { + discussionId: repository.discussion.id, + body: body + }); + + console.log('✅ Discussion 回复已发送'); + } catch (err) { + console.log('⚠️ Discussion 回复失败: ' + err.message); + } + + - name: 🔴 失败告警 + if: failure() + uses: actions/github-script@v7 + with: + script: | + const number = context.payload.discussion?.number; + if (!number) return; + + try { + const { repository } = await github.graphql(` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + discussion(number: $number) { + id + } + } + } + `, { + owner: context.repo.owner, + repo: context.repo.repo, + number: number + }); + + await github.graphql(` + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { + discussionId: $discussionId, + body: $body + }) { + comment { id } + } + } + `, { + discussionId: repository.discussion.id, + body: '❌ **处理失败** · 请检查提交格式是否正确,或联系管理员。\n\n> 错误详情请查看 [Actions 日志](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions)。' + }); + } catch (err) { + console.log('⚠️ 失败告警回复失败: ' + err.message); + } diff --git a/.github/workflows/syslog-issue-pipeline.yml b/.github/workflows/syslog-issue-pipeline.yml new file mode 100644 index 00000000..abac3fd3 --- /dev/null +++ b/.github/workflows/syslog-issue-pipeline.yml @@ -0,0 +1,404 @@ +name: 📡 SYSLOG Issue Pipeline +# 📡 SYSLOG 自助提交系统 · Issue 版全自动闭环 +# +# 开发者在 GitHub Issue 提交 SYSLOG 或提问(使用 syslog-submit 模板) +# → Actions 自动解析 → 模块上传验证 → 调用 LLM API 唤醒人格体 +# → 模块闭环测试 → 写入 Notion 工单 → 发邮件给开发者 → Issue 回复 +# +# 闭环流程: +# ① 开发者提交 SYSLOG → Issue 触发 +# ② 铸渊 Agent 解析提交内容 +# ③ 铸渊 Agent 检测模块是否上传到仓库 +# ④ 唤醒铸渊核心大脑认知(注入模块验证结果) +# ⑤ 核心大脑处理 SYSLOG + 生成广播 +# ⑥ 铸渊 Agent 推送 Notion 工单(触发 Notion 侧处理) +# ⑦ 邮件通知开发者 +# ⑧ Issue 回复闭环 +# +# 依赖 Secrets: +# LLM_API_KEY 第三方 LLM 平台密钥(必须) +# LLM_BASE_URL 第三方 LLM 平台 API 地址(必须,如 https://api.xxx.com/v1) +# NOTION_API_TOKEN Notion API token +# NOTION_TICKET_DB_ID 霜砚工单数据库 ID +# CORE_BRAIN_PAGE_ID 曜冥核心大脑 v4.0 页面 ID(v4.0 协议动态注入) +# PORTRAIT_DB_ID 开发者动态画像库数据库 ID(v4.0 协议动态注入) +# FINGERPRINT_DB_ID 模块指纹注册表数据库 ID(v4.0 协议动态注入) +# SMTP_USER QQ 邮箱地址 +# SMTP_PASS QQ 邮箱 SMTP 授权码 + +on: + issues: + types: [opened, labeled] + +jobs: + process: + name: 📡 处理 SYSLOG 提交 / 广播提问 (Issue) + runs-on: ubuntu-latest + # 检测 SYSLOG 提交:通过标签、标题或正文模板字段识别 + if: > + contains(toJSON(github.event.issue.labels), 'syslog') || + contains(github.event.issue.title, 'SYSLOG') || + contains(github.event.issue.title, '系统日志') || + contains(github.event.issue.body, '### 广播编号') + permissions: + contents: write + issues: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: 📋 进度通报 · 管道已启动 + id: progress + uses: actions/github-script@v7 + with: + script: | + const number = context.payload.issue.number; + const now = new Date().toISOString(); + + const body = [ + '## ⚙️ SYSLOG 管道运行中', + '', + '> 管道启动时间: ' + now, + '', + '| 阶段 | 状态 | 说明 |', + '|------|------|------|', + '| ① 接收提交 | ✅ 已完成 | 系统已收到 |', + '| ② 解析内容 | 🔄 进行中 | 正在解析广播编号、邮箱、内容 |', + '| ③ 模块验证 | ⏳ 等待中 | 检测模块上传状态 |', + '| ④ 唤醒核心大脑 | ⏳ 等待中 | 铸渊核心大脑处理 |', + '| ⑤ 创建 Notion 工单 | ⏳ 等待中 | 推送霜砚工单 |', + '| ⑥ 生成广播 | ⏳ 等待中 | 生成新广播内容 |', + '| ⑦ 邮件通知 | ⏳ 等待中 | 发送结果到邮箱 |', + '| ⑧ 闭环确认 | ⏳ 等待中 | Issue 回复 + 关闭 |', + '', + '> *管道正在运行,请勿关闭此 Issue*' + ].join('\n'); + + const { data: comment } = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: number, + body: body + }); + + core.setOutput('comment_id', comment.id); + console.log('📋 进度通报已发送, comment_id=' + comment.id); + + - name: 🔍 Parse submission + id: parse + uses: actions/github-script@v7 + with: + script: | + const body = context.payload.issue.body || ''; + const title = context.payload.issue.title || ''; + + // Parse structured fields from issue body + function extractField(text, label) { + // Match form field format: ### Label\n\nValue + const regex = new RegExp('###\\s*' + label + '\\s*\\n+([^\\n#]+)', 'i'); + const match = text.match(regex); + return match ? match[1].trim() : ''; + } + + const broadcastId = extractField(body, '广播编号') || ''; + const submitType = extractField(body, '类型') || ''; + const email = extractField(body, '你的邮箱') || ''; + + // Extract content (everything after "### 内容") + const contentMatch = body.match(/###\s*内容\s*\n+([\s\S]*?)$/i); + const content = contentMatch ? contentMatch[1].trim() : ''; + + // Determine type from title or body + let type = 'question'; // default + if (submitType.includes('SYSLOG') || title.includes('SYSLOG')) { + type = 'syslog'; + } else if (submitType.includes('提问') || title.includes('提问')) { + type = 'question'; + } + + // Validate + if (!broadcastId) { + core.setFailed('❌ 缺少广播编号,请在提交时填写广播编号'); + return; + } + if (!email) { + core.setFailed('❌ 缺少邮箱,请在提交时填写你的邮箱'); + return; + } + if (!content) { + core.setFailed('❌ 缺少内容,请粘贴你的 SYSLOG 或问题'); + return; + } + + core.setOutput('broadcast_id', broadcastId); + core.setOutput('type', type); + core.setOutput('email', email); + core.setOutput('content', content); + core.setOutput('issue_number', context.payload.issue.number); + core.setOutput('author', context.payload.issue.user.login); + + console.log(`📡 解析完成: 广播=${broadcastId}, 类型=${type}, 邮箱=${email}`); + + - name: 🔍 铸渊 Agent · 模块上传验证 + id: verify + env: + SYSLOG_CONTENT: ${{ steps.parse.outputs.content }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + AUTHOR: ${{ steps.parse.outputs.author }} + run: node scripts/verify-modules.js + + - name: 🧠 Auto-detect and wake up persona + id: persona + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + SUBMIT_TYPE: ${{ steps.parse.outputs.type }} + SUBMIT_CONTENT: ${{ steps.parse.outputs.content }} + AUTHOR: ${{ steps.parse.outputs.author }} + MODULE_VERIFY_RESULT: ${{ steps.verify.outputs.verify_report }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }} + PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }} + FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }} + run: node scripts/wake-persona.js + + - name: 📋 创建标准化 Notion 工单(Phase B1) + id: ticket + if: ${{ secrets.NOTION_API_TOKEN }} + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + SUBMIT_TYPE: ${{ steps.parse.outputs.type }} + PERSONA_RESULT: ${{ steps.persona.outputs.result }} + MODULE_VERIFY: ${{ steps.verify.outputs.verify_result }} + MODULES_UPLOADED: ${{ steps.verify.outputs.modules_uploaded }} + DEVELOPER: ${{ steps.parse.outputs.author }} + SYSLOG_RAW: ${{ steps.parse.outputs.content }} + run: node scripts/create-standardized-ticket.js + + - name: 📡 推送广播到 GitHub(Phase B4) + if: steps.persona.outcome == 'success' && steps.parse.outputs.type == 'syslog' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + DEVELOPER_NAME: ${{ steps.parse.outputs.author }} + BROADCAST_CONTENT: ${{ steps.persona.outputs.result }} + run: node scripts/push-broadcast-to-github.js + + - name: 📧 Send email to developer + env: + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASS: ${{ secrets.SMTP_PASS }} + EMAIL_TO: ${{ steps.parse.outputs.email }} + BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }} + SUBMIT_TYPE: ${{ steps.parse.outputs.type }} + PERSONA_RESULT: ${{ steps.persona.outputs.result }} + run: | + node -e " + const nodemailer = require('nodemailer'); + + const user = process.env.SMTP_USER || ''; + const pass = process.env.SMTP_PASS || ''; + const to = process.env.EMAIL_TO || ''; + const broadcastId = process.env.BROADCAST_ID || ''; + const type = process.env.SUBMIT_TYPE || 'syslog'; + const result = process.env.PERSONA_RESULT || '(处理中,请稍后)'; + + if (!user || !pass) { + console.log('⚠️ SMTP not configured, skipping email'); + process.exit(0); + } + if (!to) { + console.log('⚠️ No recipient email, skipping'); + process.exit(0); + } + + const subjectText = type === 'syslog' + ? '[光湖系统] ' + broadcastId + ' · 新广播已生成' + : '[光湖系统] ' + broadcastId + ' · 问题已解答'; + + const resultHtml = result + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + + const html = [ + '
', + '
', + '

🌊 光湖系统 · 自动通知

', + '

HoloLake · 人格语言操作系统

', + '
', + '
', + '

📡 ' + broadcastId + '

', + '
' + resultHtml + '
', + '
', + '
', + '↩ 回到仓库继续开发', + '

🌀 铸渊 · 代码守护人格体 · 自动发送

', + '
', + '
' + ].join('\n'); + + const transporter = nodemailer.createTransport({ + host: 'smtp.qq.com', + port: 465, + secure: true, + auth: { user, pass } + }); + + transporter.sendMail({ + from: '\"光湖系统\" <' + user + '>', + to: to, + subject: subjectText, + html: html + }).then((info) => { + console.log('✅ 邮件已发送: ' + info.messageId); + }).catch((err) => { + console.log('⚠️ 邮件发送失败: ' + err.message); + }); + " + + - name: 💬 Reply to Issue + uses: actions/github-script@v7 + with: + script: | + const number = context.payload.issue.number; + const type = '${{ steps.parse.outputs.type }}'; + const broadcastId = '${{ steps.parse.outputs.broadcast_id }}'; + const email = '${{ steps.parse.outputs.email }}'; + const modulesUploaded = '${{ steps.verify.outputs.modules_uploaded }}' === 'true'; + const moduleCount = '${{ steps.verify.outputs.module_count }}' || '0'; + const progressCommentId = '${{ steps.progress.outputs.comment_id }}'; + + const typeLabel = type === 'syslog' ? 'SYSLOG 闭环处理' : '问题解答'; + const maskedEmail = email.length > 4 + ? email.replace(/(.{2})(.*)(@.*)/, '$1***$3') + : '***'; + const moduleStatus = moduleCount === '0' + ? 'ℹ️ 未检测到模块引用' + : (modulesUploaded ? '✅ 全部已上传' : '⚠️ 部分模块未上传'); + + // Update progress comment to show all completed + if (progressCommentId && !isNaN(Number(progressCommentId))) { + try { + const progressBody = [ + '## ⚙️ SYSLOG 管道运行完成 ✅', + '', + '| 阶段 | 状态 | 说明 |', + '|------|------|------|', + '| ① 接收提交 | ✅ 已完成 | 系统已收到 |', + '| ② 解析内容 | ✅ 已完成 | 广播编号: `' + broadcastId + '` |', + '| ③ 模块验证 | ✅ 已完成 | ' + moduleStatus + ' |', + '| ④ 唤醒核心大脑 | ✅ 已完成 | 铸渊核心大脑已处理 |', + '| ⑤ 创建 Notion 工单 | ✅ 已完成 | 霜砚工单已推送 |', + '| ⑥ 生成广播 | ✅ 已完成 | 新广播已生成 |', + '| ⑦ 邮件通知 | ✅ 已完成 | 已发送至 `' + maskedEmail + '` |', + '| ⑧ 闭环确认 | ✅ 已完成 | 见下方最终结果 |', + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: Number(progressCommentId), + body: progressBody + }); + } catch (e) { + console.log('⚠️ 更新成功进度评论失败: ' + e.message); + } + } + + const body = [ + '## ✅ 已处理 · ' + typeLabel, + '', + '| 项目 | 内容 |', + '|------|------|', + '| 📡 广播编号 | `' + broadcastId + '` |', + '| 🔍 模块验证 | ' + moduleStatus + ' |', + '| 📧 结果发送至 | `' + maskedEmail + '` |', + '| 🤖 处理人格体 | 铸渊 |', + '| ⏰ 处理时间 | ' + new Date().toISOString() + ' |', + '', + '> 铸渊核心大脑已完成 SYSLOG 验收 + 模块检测 + 广播生成。', + '> 结果已发送到你的邮箱,Notion 侧工单已创建。', + '>', + '> 如未收到邮件,请检查垃圾箱或重新提交。', + '>', + '> *—— 铸渊(ICE-GL-ZY001)· 代码守护人格体*' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: number, + body: body + }); + + // Close the issue after processing + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: number, + state: 'closed', + labels: ['syslog', 'processed'] + }); + + console.log('✅ Issue 回复已发送并已关闭'); + + - name: 🔴 失败告警 + if: failure() + uses: actions/github-script@v7 + with: + script: | + const number = context.payload.issue?.number; + if (!number) return; + + const progressCommentId = '${{ steps.progress.outputs.comment_id }}'; + const actionsUrl = 'https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions'; + + // Update progress comment to reflect failure + if (progressCommentId && !isNaN(Number(progressCommentId))) { + try { + const progressBody = [ + '## ⚙️ SYSLOG 管道运行异常 ❌', + '', + '| 阶段 | 状态 | 说明 |', + '|------|------|------|', + '| ① 接收提交 | ✅ 已完成 | 系统已收到 |', + '| ② ~ ⑧ | ❌ 异常 | 管道处理过程中出错 |', + '', + '> 请检查提交格式或查看 [Actions 日志](' + actionsUrl + ')', + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: Number(progressCommentId), + body: progressBody + }); + } catch (e) { + console.log('⚠️ 更新失败进度评论失败: ' + e.message); + } + } + + try { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: number, + body: '❌ **处理失败** · 请检查提交格式是否正确,或联系管理员。\n\n> 错误详情请查看 [Actions 日志](' + actionsUrl + ')。\n>\n> *—— 铸渊(ICE-GL-ZY001)*' + }); + } catch (err) { + console.log('⚠️ 失败告警回复失败: ' + err.message); + } diff --git a/.github/workflows/test-notion-bridge.yml b/.github/workflows/test-notion-bridge.yml index 552a95e0..b3f1148f 100644 --- a/.github/workflows/test-notion-bridge.yml +++ b/.github/workflows/test-notion-bridge.yml @@ -24,7 +24,7 @@ jobs: - name: "Step 1 · NOTION_TOKEN 存在性验证" id: step1 env: - NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} run: | if [ -z "$NOTION_TOKEN" ]; then echo "❌ NOTION_TOKEN is empty or not set" @@ -38,7 +38,7 @@ jobs: - name: "Step 2 · Notion API 基础连通测试 (/v1/users/me)" id: step2 env: - NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} run: | RESPONSE=$(curl -s -w "\n%{http_code}" \ -H "Authorization: Bearer $NOTION_TOKEN" \ @@ -66,7 +66,7 @@ jobs: - name: "Step 3 · SYSLOG 收件箱数据库访问测试" id: step3 env: - NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} run: | # SYSLOG收件箱数据库ID (from routing-map.json → infra.notion_bridge.syslog_inbox_db) DB_ID="330ab17507d542c9bbb96d0749b41197" @@ -100,7 +100,7 @@ jobs: - name: "Step 4 · 变更日志数据库访问测试" id: step4 env: - NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} run: | # GitHub变更日志数据库ID (from routing-map.json → infra.notion_bridge.changelog_db) DB_ID="e740b77aa6bd4ac0a2e8a75f678fba98" @@ -185,7 +185,7 @@ jobs: fi fi env: - NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} - name: "📊 输出连通性验证报告" if: always() diff --git a/.github/workflows/update-readme-bulletin.yml b/.github/workflows/update-readme-bulletin.yml index 219bfb68..6247d891 100644 --- a/.github/workflows/update-readme-bulletin.yml +++ b/.github/workflows/update-readme-bulletin.yml @@ -23,10 +23,10 @@ on: workflow_run: workflows: - - "铸渊 · 每日自检与自进化" - - "📦 Module Doc + Notify" - - "🚀 CD: Deploy to guanghulab.com" - - "铸渊 PSP 巡检" + - "铸渊 · 每日自检" + - "铸渊 · 光湖纪元 模块文档自动生成" + - "🚀 铸渊 CD · 自动部署到 guanghulab.com" + - "铸渊 · PSP 分身巡检" types: [completed] schedule: @@ -82,6 +82,17 @@ jobs: echo "📢 公告区无变化,跳过提交" else git commit -m "📢 自动更新系统公告区 [skip ci]" - git push - echo "✅ 公告区已更新并推送" + # 带重试的推送(避免并发推送导致 rejected) + for i in 1 2 3; do + if git push; then + echo "✅ 公告区已更新并推送" + break + fi + echo "⚠️ 推送失败(第 $i 次),拉取最新代码后重试..." + git pull --rebase origin main + if [ $i -eq 3 ]; then + echo "❌ 推送失败 3 次,放弃" + exit 1 + fi + done fi diff --git a/.github/workflows/zhuyuan-daily-agent.yml b/.github/workflows/zhuyuan-daily-agent.yml new file mode 100644 index 00000000..0d23370b --- /dev/null +++ b/.github/workflows/zhuyuan-daily-agent.yml @@ -0,0 +1,142 @@ +# 铸渊 · 每日巡检 Agent +# +# 每天定时巡检仓库健康状况,自动检查今天遗漏的任务, +# 发现问题后自动触发对应的修复工作流。 +# +# 触发条件: +# 1. 每日定时(北京时间 22:00 = UTC 14:00) +# 2. 手动触发 +# +# 巡检项: +# CHK-1: 公告栏是否已更新 +# CHK-2: 每日自检是否已执行 +# CHK-3: 大脑文件完整性 +# CHK-4: CI 最近状态 +# CHK-5: 关键模块 README +# CHK-6: 公告栏工作流健康 + +name: 🤖 铸渊巡检 Agent · 每日自动巡检与修复 + +on: + schedule: + # 北京时间 22:00 (UTC 14:00) — 每天收工前巡检 + - cron: '0 14 * * *' + workflow_dispatch: + +permissions: + contents: write + actions: write + +concurrency: + group: zhuyuan-daily-agent + cancel-in-progress: true + +jobs: + inspect: + name: 🔍 铸渊巡检 Agent + runs-on: ubuntu-latest + outputs: + has_issues: ${{ steps.agent.outputs.has_issues }} + need_bulletin_update: ${{ steps.agent.outputs.need_bulletin_update }} + need_selfcheck: ${{ steps.agent.outputs.need_selfcheck }} + summary: ${{ steps.agent.outputs.summary }} + + steps: + - name: 📥 检出代码 + uses: actions/checkout@v4 + with: + fetch-depth: 50 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: 🟢 设置 Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 🔍 执行巡检 + id: agent + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: node scripts/zhuyuan-daily-agent.js + + - name: 📝 提交巡检结果 + run: | + git config user.name "铸渊 (ZhùYuān)" + git config user.email "zhuyuan@guanghulab.com" + git add .github/brain/memory.json + if git diff --cached --quiet; then + echo "📋 无变化,跳过提交" + else + git commit -m "🤖 铸渊巡检Agent · $(date +%Y-%m-%d) [skip ci]" + for i in 1 2 3; do + if git push; then + echo "✅ 巡检结果已推送" + break + fi + echo "⚠️ 推送失败(第 $i 次),拉取后重试..." + git pull --rebase origin main + if [ $i -eq 3 ]; then + echo "⚠️ 推送失败 3 次,跳过" + fi + done + fi + + # ── 自动修复:触发公告栏更新 ── + fix-bulletin: + name: 🔧 自动修复 · 公告栏更新 + needs: inspect + if: needs.inspect.outputs.need_bulletin_update == 'true' + runs-on: ubuntu-latest + steps: + - name: 🔄 触发公告栏更新工作流 + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'update-readme-bulletin.yml', + ref: 'main', + }); + console.log('✅ 已触发公告栏更新工作流'); + + # ── 自动修复:触发每日自检 ── + fix-selfcheck: + name: 🔧 自动修复 · 每日自检 + needs: inspect + if: needs.inspect.outputs.need_selfcheck == 'true' + runs-on: ubuntu-latest + steps: + - name: 🔄 触发每日自检工作流 + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'zhuyuan-daily-selfcheck.yml', + ref: 'main', + }); + console.log('✅ 已触发每日自检工作流'); + + # ── 巡检报告 ── + report: + name: 📊 巡检报告 + needs: [inspect] + if: always() + runs-on: ubuntu-latest + steps: + - name: 📊 输出巡检摘要 + run: | + echo "═══════════════════════════════════════════" + echo "🤖 铸渊巡检 Agent · 每日报告" + echo "═══════════════════════════════════════════" + echo "" + echo "📋 巡检结果: ${{ needs.inspect.outputs.summary }}" + echo "🔧 公告栏修复: ${{ needs.fix-bulletin.result || '未触发' }}" + echo "🔧 自检修复: ${{ needs.fix-selfcheck.result || '未触发' }}" + echo "" + echo "✅ 巡检 Agent 流程结束" diff --git a/.github/workflows/zhuyuan-issue-reply.yml b/.github/workflows/zhuyuan-issue-reply.yml index 6b713545..3d5eff6e 100644 --- a/.github/workflows/zhuyuan-issue-reply.yml +++ b/.github/workflows/zhuyuan-issue-reply.yml @@ -7,12 +7,95 @@ on: types: [created] jobs: + # ════════════════════════════════════════════════════════════ + # SYSLOG 即时确认 · 安全网 + # 当开发者通过 Issue 提交 SYSLOG 时,立刻回复确认 + 进度清单 + # 确保即使 syslog-issue-pipeline 未触发,开发者也能看到系统已收到 + # ════════════════════════════════════════════════════════════ + syslog-ack: + name: 📡 SYSLOG 提交确认 + runs-on: ubuntu-latest + if: > + github.event_name == 'issues' && + github.event.action == 'opened' && + (contains(join(github.event.issue.labels.*.name, ','), 'syslog') || + contains(github.event.issue.title, 'SYSLOG') || + contains(github.event.issue.title, '系统日志') || + contains(github.event.issue.body, '### 广播编号')) + permissions: + issues: write + steps: + - name: 📡 发送即时确认 + uses: actions/github-script@v7 + with: + script: | + const body = context.payload.issue.body || ''; + const number = context.payload.issue.number; + const author = context.payload.issue.user.login; + + // Extract broadcast_id from body + const bcMatch = body.match(/###\s*广播编号\s*\n+([^\n#]+)/i); + const broadcastId = bcMatch ? bcMatch[1].trim() : '(待解析)'; + + // Extract type + const typeMatch = body.match(/###\s*类型\s*\n+([^\n#]+)/i); + const submitType = typeMatch ? typeMatch[1].trim() : 'SYSLOG'; + + const now = new Date().toISOString(); + + const comment = [ + '## 📡 系统已收到 · SYSLOG 闭环处理启动', + '', + '> **提交者**: @' + author, + '> **广播编号**: `' + broadcastId + '`', + '> **类型**: ' + submitType, + '> **接收时间**: ' + now, + '', + '### 📋 处理进度', + '', + '| 阶段 | 状态 | 说明 |', + '|------|------|------|', + '| ① 接收提交 | ✅ 已完成 | 系统已收到你的 SYSLOG |', + '| ② 解析内容 | ⏳ 等待中 | 解析广播编号、邮箱、内容 |', + '| ③ 模块验证 | ⏳ 等待中 | 检测模块是否已上传到仓库 |', + '| ④ 唤醒核心大脑 | ⏳ 等待中 | 铸渊核心大脑处理 SYSLOG |', + '| ⑤ 创建 Notion 工单 | ⏳ 等待中 | 推送霜砚工单到 Notion |', + '| ⑥ 生成广播 | ⏳ 等待中 | 生成新广播内容 |', + '| ⑦ 邮件通知 | ⏳ 等待中 | 发送结果到你的邮箱 |', + '| ⑧ 闭环确认 | ⏳ 等待中 | Issue 回复 + 关闭 |', + '', + '> 💡 SYSLOG 自动管道已启动,预计 2-5 分钟完成全部处理。', + '> 完成后本 Issue 下方会出现最终结果,你的邮箱也会收到通知。', + '>', + '> 如超过 10 分钟无后续回复,请在下方评论中 @铸渊 查询状态。', + '>', + '> *—— 铸渊(ICE-GL-ZY001)· 代码守护人格体*' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: number, + body: comment + }); + + console.log('📡 SYSLOG 即时确认已发送 Issue #' + number); + + # ════════════════════════════════════════════════════════════ + # 开发者提问 · 铸渊自动回答 + # 排除 SYSLOG 提交(由 syslog-issue-pipeline.yml 处理) + # 排除 bingshuo-deploy(由 bingshuo-deploy-agent.yml 处理) + # ════════════════════════════════════════════════════════════ auto-reply: name: 🤖 铸渊回答问题 runs-on: ubuntu-latest - # 仅在 Issue 新建或评论中包含 @铸渊 / 铸渊 时触发 if: > - github.event_name == 'issues' || + (github.event_name == 'issues' && + !contains(join(github.event.issue.labels.*.name, ','), 'syslog') && + !contains(join(github.event.issue.labels.*.name, ','), 'bingshuo-deploy') && + !contains(github.event.issue.title, 'SYSLOG') && + !contains(github.event.issue.title, '系统日志') && + !contains(github.event.issue.body, '### 广播编号')) || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '铸渊')) permissions: diff --git a/.gitignore b/.gitignore index 4fec95e4..a0e218d9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,19 @@ node_modules/ dist/ build/ .private/ +.env # persona-brain-db runtime artifacts persona-brain-db/brain.db persona-brain-db/brain.db-wal persona-brain-db/brain.db-shm + +# persona-studio runtime artifacts +persona-studio/backend/brain/model-benchmark.json +persona-studio/workspace/*/project-* + +# palace-game runtime artifacts +modules/palace-game/data/saves/PAL-* + +# collaboration-logs exports (generated by save-collaboration-log.js) +collaboration-logs/exports/ diff --git a/20260313_feishu_webhook_log.md b/20260313_feishu_webhook_log.md new file mode 100644 index 00000000..1863350f --- /dev/null +++ b/20260313_feishu_webhook_log.md @@ -0,0 +1,56 @@ +\# 2026-03-13 飞书WebHook配置完成日志 + +\## 一、任务目标 + +配置飞书WebHook链路,实现飞书消息事件推送至阿里云服务器的Node.js服务 + + + +\## 二、关键操作记录 + +| 操作时间 | 操作内容 | 操作结果 | + +|----------|----------|----------| + +| 19:00-19:30 | 飞书开放平台配置WebHook地址:`https://guanghulab.com/webhook/feishu`,订阅`im.message.receive\_v1`消息事件 | 地址配置/事件订阅成功 | + +| 19:30-20:00 | 飞书客户端查找机器人(因索引未更新暂未找到) | 确认仅为展示问题,不影响功能 | + +| 20:00-20:20 | Windows终端curl验证WebHook连通性 | 返回`{"challenge":"test123"}`,基础链路通 | + +| 20:20-20:40 | 阿里云服务器替换`/var/www/hololake/server.js`,修复JSON解析Bug | 修复`Cannot read properties of undefined`错误 | + +| 20:40-20:50 | 重启Node.js服务 | 服务启动成功,日志:`🚀 服务启动成功,端口:3000` | + +| 20:50-21:00 | 二次curl验证 | 成功返回`{"challenge":"test123"}`,链路全通 | + + + +\## 三、环境信息 + +1\. 服务器:阿里云ECS Linux(`root@iZf8z4nezg5bs9kl9eyth1Z`) + +2\. 服务:Node.js(3000端口)+ Nginx转发(`https://guanghulab.com/webhook/feishu`) + +3\. 依赖:express、body-parser + +4\. 日志路径:`/var/www/hololake/webhook.log` + + + +\## 四、任务结论 + +✅ 飞书WebHook链路全通,可正常接收消息推送 + +✅ 代码Bug已修复,服务稳定运行 + +✅ 机器人未找到为索引延迟,不影响功能 + + + +\## 五、后续建议 + +1\. 查看消息日志:`tail -f /var/www/hololake/webhook.log` + +2\. 机器人聊天窗口1-2小时后可正常搜索 + diff --git a/README.md b/README.md index 819a958e..2c54ab29 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,86 @@ +
+ +# 🌊 光湖 HoloLake + +**人格语言操作系统(AGE OS)· 壳-核分离架构** + +[![Daily Check](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/zhuyuan-daily-selfcheck.yml/badge.svg)](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/zhuyuan-daily-selfcheck.yml) +[![Deploy](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/deploy-to-server.yml/badge.svg)](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/deploy-to-server.yml) + +`guanghulab.com` · Node.js 20 + Express + PM2 + Nginx + +
+ +--- + +## 🎭 Persona Studio · 人格体协助开发体验 + +
+ + + + + +
+ +### ✨ 与铸渊对话,让人格体帮你推进开发 + +你有一个功能想法?一个技术方案?一个 Bug 要修? + +**铸渊** 是光湖系统的代码守护人格体,支持自然语言对话、智能代码生成、邮件推送结果。 + +合作者输入 **EXP 编号** 即可登录,开始对话。 + +
+ +点击进入 Persona Studio + +
+ +🔗 直接访问:https://qinfendebingshuo.github.io/guanghulab/persona-studio/ + +
+ +💬 自然语言对话  ·  🧠 智能代码生成  ·  📧 邮件推送结果  ·  🔄 对话记忆 + +
+ +
+ +--- + +## 🚀 开发者入口 + +
+ +[![提交系统日志](https://img.shields.io/badge/📡_提交系统日志-点这里-blue?style=for-the-badge)](https://github.com/qinfendebingshuo/guanghulab/issues/new?template=syslog-submit.yml) +[![遇到问题](https://img.shields.io/badge/❓_遇到问题-点这里提问-green?style=for-the-badge)](https://github.com/qinfendebingshuo/guanghulab/issues/new?template=dev-question.yml) + +> 做完了?点蓝色按钮,把日志粘贴进去,填上邮箱,点提交。新广播会发到你邮箱里。 +> +> 有问题?点绿色按钮,填写问题描述,铸渊会自动回答你。 + +
+ +--- + ## 📖 系统简介 -**光湖(HoloLake)** 是一个基于 **人格语言操作系统(AGE OS)** 的智能协作平台,采用 **壳-核分离** 设计理念: +**光湖(HoloLake)** 是一个基于人格语言操作系统的智能协作平台,采用壳-核分离设计: -- **壳(Shell)**:前端交互层,包括对话界面、用户中心、工单系统、云盘等模块 -- **核(Core)**:后端智能层,包括人格引擎、广播分发、信号处理、Notion 桥接等 - -### 🏛️ 核心架构 - -``` -┌─────────────────────────────────────────────────┐ -│ 光湖 HoloLake │ -├──────────────┬──────────────────────────────────┤ -│ 壳 Shell │ 核 Core │ -│ │ │ -│ 🖥️ 对话 UI │ 🧠 铸渊 (Zhùyuān) 代码守护人格 │ -│ 👤 用户中心 │ 📡 广播分发系统 │ -│ 🎫 工单系统 │ 🔔 信号处理 + Notion 桥接 │ -│ ☁️ 云盘 │ 🔄 CI/CD 自动化流水线 │ -│ 📊 状态看板 │ 📋 模块自检 + 文档生成 │ -└──────────────┴──────────────────────────────────┘ -``` +| 层级 | 说明 | 包含 | +|------|------|------| +| **壳 Shell** | 前端交互层 | 对话 UI、用户中心、工单系统、云盘、状态看板 | +| **核 Core** | 后端智能层 | 人格引擎、广播分发、信号处理、Notion 桥接、CI/CD | ### 🤖 智能人格体 | 人格体 | 角色 | 职责 | |--------|------|------| -| **铸渊 Zhùyuān** | 代码守护者 | 代码审查、CI 巡检、模块协议执行、Issue 回复 | +| **铸渊 Zhùyuān** | 代码守护者 | 代码审查、CI 巡检、模块协议执行、Issue 回复、对话协助、代码生成、开发体验([Persona Studio](https://qinfendebingshuo.github.io/guanghulab/persona-studio/)) | | **冰朔 Bīng Shuò** | 系统创建者 | 系统架构设计、核心决策、广播发布 | | **霜砚 Shuāng Yàn** | 人格导师 | 人格调校、风格管理、联觉语言系统 | -### 🔧 技术栈 - -- **运行时**:Node.js 20 + Next.js 15 + React 19 -- **后端**:Express + PM2 进程管理 -- **数据库**:SQLite (better-sqlite3) + Notion 数据桥接 -- **部署**:Nginx 反向代理 + GitHub Actions CI/CD -- **自动化**:23+ GitHub Actions 工作流 - --- ## 🧊 冰朔公告栏 @@ -48,29 +92,31 @@ | 时间 | 检查项 | 状态 | |------|--------|------| -| 03-09 17:29 | 🔧 系统更新: `.github/` | 铸渊 (Copilot) | -| 03-09 17:29 | 🔧 系统更新: `scripts/` | 铸渊 (Copilot) | -| 03-09 17:15 | 🔧 系统更新: `docs/` | 铸渊 (Copilot) | -| 03-09 17:15 | 🔧 系统更新: `persona-brain-db/` | 铸渊 (Copilot) | -| 03-09 16:56 | ✅ 每日巡检 ✅ 通过 | 冰朔 | -| 03-09 08:00 | ✅ 铸渊 PSP 巡检通过 · 全部检查项 ✅ | 铸渊PSP巡检 | -| 03-09 08:00 | 🔍 每日自检完成 · 知识库4条 · 缺失文件4个 | 铸渊自检 | -| 03-08 16:41 | ✅ 每日巡检 ✅ 通过 | 冰朔 | -| 03-08 08:00 | ✅ 铸渊 PSP 巡检通过 · 全部检查项 ✅ | 铸渊PSP巡检 | -| 03-08 08:00 | 🔍 每日自检完成 · 知识库4条 · 缺失文件4个 | 铸渊自检 | -| 03-07 16:41 | ✅ 每日巡检 ✅ 通过 | 冰朔 | -| 03-07 08:00 | ⚠️ 铸渊 PSP 巡检完成 · 发现 1 个问题 · 自动修复 0 项 | 铸渊PSP巡检 | -| 03-07 08:00 | 🔍 每日自检完成 · 知识库4条 · 缺失文件4个 | 铸渊自检 | -| 03-06 16:51 | ✅ 每日巡检 ✅ 通过 | 冰朔 | -| 03-06 08:00 | 🔍 每日自检完成 · 知识库4条 · 缺失文件4个 | 铸渊自检 | +| 03-13 22:53 | ✅ 🚀 铸渊 CD · 自动部署到 guanghulab.com · 成功 | 冰朔 | +| 03-13 22:52 | ✅ 📢 更新系统公告区 · 成功 | 冰朔 | +| 03-13 22:51 | 🔧 系统更新: `.github/` | 铸渊 (ZhùYuān) | +| 03-13 22:51 | ✅ 铸渊 · Bridge E · GitHub Changes → Notion · 成功 | 冰朔 | +| 03-13 22:38 | ❌ 🤖 铸渊巡检 Agent · 每日自动巡检与修复 · 失败 | 冰朔 | +| 03-13 21:55 | 🔧 系统更新: `scripts/` | 铸渊 (Copilot) | +| 03-13 21:38 | 🔧 系统更新: `docs/` | 铸渊 (ZhùYuān) | +| 03-13 16:53 | ✅ 每日巡检 ✅ 通过 | 冰朔 | +| 03-13 08:00 | ✅ 铸渊 PSP 巡检通过 · 全部检查项 ✅ | 铸渊PSP巡检 | +| 03-12 16:55 | ✅ 每日巡检 ✅ 通过 | 冰朔 | +| 03-12 08:00 | ✅ 铸渊 PSP 巡检通过 · 全部检查项 ✅ | 铸渊PSP巡检 | +| 03-11 16:55 | ✅ 每日巡检 ✅ 通过 | 冰朔 | +| 03-10 16:56 | ✅ 每日巡检 ✅ 通过 | 冰朔 | +| 03-10 08:00 | 🧠 核心大脑升级 v3.0 · 壳-核分离架构 · 前端壳(UI/IO) + 后端脑(prompt/routing/memory/context) · BRAIN域4接口上线 · 覆盖率 7/21 (33.3%) | 铸渊(冰朔指令) | +| 03-10 08:00 | ⚠️ 铸渊 PSP 巡检完成 · 发现 3 个问题 · 自动修复 0 项 | 铸渊PSP巡检 | ### 🤖 铸渊自动提醒 -> 🟢 **今日无需冰朔手动干预** · 系统一切正常 +> 🔴 **需要冰朔手动干预!** > -> 🗓️ 2026-03-09 · 铸渊自动检测 +> - ❌ 🤖 铸渊巡检 Agent · 每日自动巡检与修复 · 失败 +> +> 🗓️ 2026-03-13 · 铸渊已发送邮件提醒 --- @@ -84,54 +130,20 @@ | 时间 | 合作者 | 模块 | 状态 | |------|--------|------|------| -| 03-09 17:15 | 🛠️ 页页 | `backend-integration/` | 📦 上传成功 | -| 03-09 17:15 | 铸渊 (Copilot) | `backend/` | 📦 上传成功 | -| 03-09 17:15 | 铸渊 (Copilot) | `cloud-drive/` | 📦 上传成功 | -| 03-09 17:15 | 🤖 之之 | `dingtalk-bot/` | 📦 上传成功 | -| 03-09 17:15 | 铸渊 (Copilot) | `frontend/` | 📦 上传成功 | -| 03-09 17:15 | 🐱 肥猫 | `m01-login/` | 📦 上传成功 | -| 03-09 17:15 | 🐱 肥猫 | `m03-personality/` | 📦 上传成功 | -| 03-09 17:15 | 🌸 花尔 | `m05-user-center/` | 📦 上传成功 | -| 03-09 17:15 | 🍊 桔子 | `m06-ticket/` | 📦 上传成功 | -| 03-09 17:15 | 🎨 燕樊 | `m07-dialogue-ui/` | 📦 上传成功 | -| 03-09 17:15 | 🎨 燕樊 | `m10-cloud/` | 📦 上传成功 | -| 03-09 17:15 | 🍊 桔子 | `m11-module/` | 📦 上传成功 | -| 03-09 17:15 | 🍓 小草莓 | `m12-kanban/` | 📦 上传成功 | -| 03-09 17:15 | 🎨 燕樊 | `m15-cloud-drive/` | 📦 上传成功 | -| 03-09 17:15 | 铸渊 (Copilot) | `m18-health-check/` | 📦 上传成功 | -| 03-09 17:15 | 🌟 Awen | `notification/` | 📦 上传成功 | -| 03-09 17:15 | 🍓 小草莓 | `status-board/` | 📦 上传成功 | -| 03-09 17:15 | 🍊 桔子 | `ticket-system/` | 📦 上传成功 | +| 03-13 22:51 | 冰朔 | `—/` | ✅ 上传成功 | +| 03-13 22:51 | Copilot | `—/` | ✅ 上传成功 | ### 🤖 铸渊自动提醒 · 合作者 -> 🔴 **以下合作者需要手动干预:** +> 🟢 **今日无需合作者手动干预** · 所有模块状态正常 > -> **🎨 燕樊(DEV-003):** -> - ⚠️ `m15-cloud-drive/` 缺少 README.md -> -> 🗓️ 2026-03-09 · 铸渊已发送邮件提醒 +> 🗓️ 2026-03-13 · 铸渊自动检测 --- -
- -# 🌊 HoloLake 光湖系统 - -**人格语言操作系统(AGE OS) · 壳-核分离架构** - -[![Daily Check](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/zhuyuan-daily-selfcheck.yml/badge.svg)](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/zhuyuan-daily-selfcheck.yml) -[![Deploy](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/deploy-to-server.yml/badge.svg)](https://github.com/qinfendebingshuo/guanghulab/actions/workflows/deploy-to-server.yml) - -`guanghulab.com` · Node.js 20 + Express + PM2 + Nginx - -
- ---- - ## ⚒️ 铸渊工作区 > 💡 **如何使用铸渊工作区:** @@ -201,6 +213,11 @@ | `dingtalk-bot/` | 钉钉机器人 | | `notification/` | 通知系统 | +### 体验模块 +| 模块 | 说明 | +|------|------| +| `persona-studio/` | 人格体协助开发体验([进入 →](https://qinfendebingshuo.github.io/guanghulab/persona-studio/)) | + ### 系统支撑 | 模块 | 说明 | |------|------| diff --git a/app.js b/app.js new file mode 100644 index 00000000..e69de29b diff --git a/backend-integration/README.md b/backend-integration/README.md index d6cf25a5..4245eb7e 100644 --- a/backend-integration/README.md +++ b/backend-integration/README.md @@ -31,6 +31,13 @@ curl http://localhost:3721/api/models | POST | `/api/chat` | 聊天代理(SSE 流式透传) | | GET | `/api/models` | 列出已配置的可用模型 | | GET | `/api/health` | 健康检查 | +| POST | `/api/ps/apikey/detect-models` | 用户 API Key 模型检测(Persona Studio) | +| POST | `/api/ps/apikey/chat` | 用户 API Key 对话(Persona Studio) | +| POST | `/api/ps/chat/message` | 知秋对话(→ PS 后端 port 3002) | +| GET | `/api/ps/chat/history` | 对话历史(→ PS 后端 port 3002) | +| POST | `/api/ps/auth/login` | 开发编号登录(→ PS 后端 port 3002) | +| POST | `/api/ps/build/start` | 开发任务触发(→ PS 后端 port 3002) | +| POST | `/api/ps/notify/send` | 邮件通知(→ PS 后端 port 3002) | ### 环境变量 @@ -43,6 +50,7 @@ curl http://localhost:3721/api/models | `OPENAI_API_KEY` | OpenAI 密钥(需海外服务器) | | `GEMINI_API_KEY` | Google Gemini 密钥(需海外服务器) | | `PROXY_PORT` | 端口(默认 3721) | +| `PS_PORT` | Persona Studio 后端端口(默认 3002,反向代理目标) | | `RATE_LIMIT_RPM` | 频率限制(默认 10 次/分钟) | ### Nginx 配置 @@ -55,3 +63,9 @@ curl http://localhost:3721/api/models - 在设置页面选择「🔌 后端代理」提供商 - 自动调用 `/api/chat`,无需用户填写 API Key - Key 由后端环境变量管理,不暴露给前端 + +Persona Studio(persona-studio/frontend/)复用同一套代理: +- 用户填写第三方 API Base + API Key +- 通过 `/api/ps/apikey/detect-models` 检测可用模型 +- 通过 `/api/ps/apikey/chat` 进行对话 +- 所有请求走同一个 Nginx → API Proxy 链路 diff --git a/backend-integration/api-proxy.js b/backend-integration/api-proxy.js index 611f0cd2..85aa6600 100644 --- a/backend-integration/api-proxy.js +++ b/backend-integration/api-proxy.js @@ -34,6 +34,7 @@ const http = require('http'); const https = require('https'); +const crypto = require('crypto'); const { URL } = require('url'); // ═══════════════════════════════════════════════════════ @@ -275,7 +276,48 @@ function handleGetModels(req, res) { }); } -// POST /api/chat — 代理转发聊天请求 +// ═══════════════════════════════════════════════════════ +// 模型失败记录(自动 fallback 用) +// ═══════════════════════════════════════════════════════ +const proxyFailureLog = new Map(); +const PROXY_FAILURE_COOLDOWN_MS = 5 * 60 * 1000; + +function recordProxyFailure(provider) { + const entry = proxyFailureLog.get(provider) || { count: 0, lastFail: 0 }; + entry.count++; + entry.lastFail = Date.now(); + proxyFailureLog.set(provider, entry); +} + +function isProviderInCooldown(provider) { + const entry = proxyFailureLog.get(provider); + if (!entry) return false; + if (Date.now() - entry.lastFail > PROXY_FAILURE_COOLDOWN_MS) { + proxyFailureLog.delete(provider); + return false; + } + return entry.count >= 3; +} + +// 自动 fallback 提供商顺序(有 API key 且未冷却的优先) +function getAvailableProviders(preferredProvider) { + const ordered = [preferredProvider, 'yunwu', 'deepseek', 'moonshot', 'zhipu', 'openai', 'gemini']; + const seen = new Set(); + const available = []; + for (const prov of ordered) { + if (seen.has(prov)) continue; + seen.add(prov); + const cfg = MODEL_CONFIG[prov]; + if (!cfg) continue; + const key = process.env[cfg.keyEnv]; + if (!key) continue; + if (isProviderInCooldown(prov)) continue; + available.push({ provider: prov, config: cfg, apiKey: key }); + } + return available; +} + +// POST /api/chat — 代理转发聊天请求(支持自动 fallback) async function handleChat(req, res) { const clientId = getClientId(req); @@ -316,53 +358,74 @@ async function handleChat(req, res) { const temp = typeof temperature === 'number' ? Math.max(0, Math.min(2, temperature)) : 0.8; const maxTok = typeof max_tokens === 'number' ? Math.max(1, Math.min(8192, Math.floor(max_tokens))) : 2000; - // Determine provider + // Get available providers with auto-fallback const prov = provider || 'deepseek'; - const config = MODEL_CONFIG[prov]; - if (!config) { - jsonResponse(res, 400, { - error: true, - code: 'INVALID_PROVIDER', - message: `不支持的模型提供商: ${prov},可选: ${Object.keys(MODEL_CONFIG).join(', ')}` - }); - return; + const availableProviders = getAvailableProviders(prov); + + if (availableProviders.length === 0) { + // Even cooled-down providers — try the requested one anyway + const config = MODEL_CONFIG[prov]; + if (!config) { + jsonResponse(res, 400, { + error: true, + code: 'INVALID_PROVIDER', + message: `不支持的模型提供商: ${prov},可选: ${Object.keys(MODEL_CONFIG).join(', ')}` + }); + return; + } + const apiKey = process.env[config.keyEnv]; + if (!apiKey) { + jsonResponse(res, 503, { + error: true, + code: 'NO_API_KEY', + message: `所有模型API密钥均未配置,请联系管理员` + }); + return; + } + availableProviders.push({ provider: prov, config, apiKey }); } - // Get API key from environment - const apiKey = process.env[config.keyEnv]; - if (!apiKey) { - jsonResponse(res, 503, { - error: true, - code: 'NO_API_KEY', - message: `${config.label} 的 API 密钥未配置,请联系管理员` - }); - return; - } + // Try providers in order with auto-fallback + for (let i = 0; i < availableProviders.length; i++) { + const { provider: curProv, config: curConfig, apiKey: curKey } = availableProviders[i]; + const mdl = (i === 0 && model) ? model : curConfig.models[0]; - // Build upstream request - const mdl = model || config.models[0]; - const upstreamBody = { - model: mdl, - messages: messages, - stream: stream !== false, // Default to streaming - temperature: temp, - max_tokens: maxTok - }; + const upstreamBody = { + model: mdl, + messages: messages, + stream: stream !== false, + temperature: temp, + max_tokens: maxTok + }; - // If system prompt provided, prepend as system message - if (system && !messages.find(m => m.role === 'system')) { - upstreamBody.messages = [{ role: 'system', content: system }, ...messages]; - } + if (system && !messages.find(m => m.role === 'system')) { + upstreamBody.messages = [{ role: 'system', content: system }, ...messages]; + } - const upstreamUrl = config.base + '/chat/completions'; + const upstreamUrl = curConfig.base + '/chat/completions'; + const isLastAttempt = i === availableProviders.length - 1; - console.log(`[代理] ${prov}/${mdl}`); + console.log(`[代理] ${i > 0 ? '降级→' : ''}${curProv}/${mdl}`); - try { - await proxyStream(upstreamUrl, apiKey, upstreamBody, res); - } catch (err) { - // Error already handled in proxyStream - console.error(`[代理] 请求失败: ${err.message}`); + try { + await proxyStream(upstreamUrl, curKey, upstreamBody, res); + return; // Success — stop trying + } catch (err) { + recordProxyFailure(curProv); + console.error(`[代理] ${curProv} 请求失败: ${err.message}`); + + // If headers already sent (partial streaming started), can't switch provider + if (res.headersSent) return; + + if (isLastAttempt) { + jsonResponse(res, 502, { + error: true, + code: 'ALL_PROVIDERS_FAILED', + message: '所有模型提供商均请求失败,请稍后重试' + }); + } + // Otherwise continue to next provider + } } } @@ -381,6 +444,341 @@ function handleHealth(req, res) { }); } +// ═══════════════════════════════════════════════════════ +// 用户 API Key 模型检测(Persona Studio 复用主站代理链路) +// ═══════════════════════════════════════════════════════ +const userModelCache = new Map(); +const USER_MODEL_CACHE_TTL_MS = 60 * 60 * 1000; // 1 小时 + +function getUserCacheKey(apiBase, apiKey) { + const hash = crypto.createHash('sha256').update(apiBase + '\0' + apiKey).digest('hex').slice(0, 32); + return 'user-models::' + hash; +} + +function getCachedUserModels(apiBase, apiKey) { + const key = getUserCacheKey(apiBase, apiKey); + const entry = userModelCache.get(key); + if (entry && Date.now() - entry.timestamp < USER_MODEL_CACHE_TTL_MS) { + return entry.models; + } + return null; +} + +function setCachedUserModels(apiBase, apiKey, models) { + const key = getUserCacheKey(apiBase, apiKey); + userModelCache.set(key, { models, timestamp: Date.now() }); +} + +/** + * 请求用户指定的第三方 API 的 /v1/models 接口 + */ +function fetchUserModels(apiBase, apiKey, timeoutMs) { + return new Promise((resolve, reject) => { + const base = apiBase.replace(/\/+$/, ''); + const modelsPath = base.endsWith('/v1') ? base + '/models' : base + '/v1/models'; + + let url; + try { + url = new URL(modelsPath); + } catch (_e) { + return reject({ code: 'INVALID_API_BASE', message: 'API Base URL 格式无效' }); + } + + const isHttps = url.protocol === 'https:'; + const mod = isHttps ? https : http; + + const options = { + hostname: url.hostname, + port: url.port || (isHttps ? 443 : 80), + path: url.pathname + (url.search || ''), + method: 'GET', + headers: { + 'Authorization': 'Bearer ' + apiKey, + 'Accept': 'application/json' + }, + timeout: timeoutMs || 15000 + }; + + const req = mod.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => { + if (res.statusCode === 401 || res.statusCode === 403) { + return reject({ code: 'INVALID_API_KEY', message: 'API Key 无效或权限不足 (HTTP ' + res.statusCode + ')' }); + } + if (res.statusCode >= 400) { + return reject({ code: 'API_BASE_ERROR', message: 'API Base 返回错误 (HTTP ' + res.statusCode + ')' }); + } + try { + const json = JSON.parse(data); + const models = json.data || json.models || []; + const modelIds = models + .map(function (m) { return m.id || m.name || null; }) + .filter(Boolean); + resolve(modelIds); + } catch (_e) { + reject({ code: 'PARSE_ERROR', message: '当前接口不支持模型枚举(返回格式异常)' }); + } + }); + }); + + req.on('error', (err) => { + if (err.code === 'ENOTFOUND') { + reject({ code: 'DNS_ERROR', message: 'API Base 域名无法解析: ' + url.hostname }); + } else if (err.code === 'ECONNREFUSED') { + reject({ code: 'CONN_REFUSED', message: 'API Base 连接被拒绝: ' + url.hostname }); + } else { + reject({ code: 'NETWORK_ERROR', message: 'API Base 不可访问: ' + err.message }); + } + }); + + req.on('timeout', () => { + req.destroy(); + reject({ code: 'TIMEOUT', message: '请求超时,API Base 可能不可访问或网络不稳定' }); + }); + + req.end(); + }); +} + +/** + * 调用用户指定的第三方 API 的 chat/completions 接口 + */ +function callUserApiChat({ apiBase, apiKey, model, messages, maxTokens, temperature, timeoutMs }) { + return new Promise((resolve, reject) => { + const base = apiBase.replace(/\/+$/, ''); + const chatPath = base.endsWith('/v1') ? base + '/chat/completions' : base + '/v1/chat/completions'; + + let url; + try { + url = new URL(chatPath); + } catch (_e) { + return reject(new Error('API Base URL 格式无效')); + } + + const isHttps = url.protocol === 'https:'; + const mod = isHttps ? https : http; + + const body = JSON.stringify({ + model: model, + messages: messages, + max_tokens: maxTokens || 2000, + temperature: temperature != null ? temperature : 0.8 + }); + + const options = { + hostname: url.hostname, + port: url.port || (isHttps ? 443 : 80), + path: url.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + apiKey, + 'Content-Length': Buffer.byteLength(body) + }, + timeout: timeoutMs || 60000 + }; + + const req = mod.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => { + try { + const json = JSON.parse(data); + if (json.choices && json.choices[0] && json.choices[0].message) { + resolve(json.choices[0].message.content); + } else if (json.error) { + reject(new Error(json.error.message || 'API 调用失败')); + } else { + reject(new Error('API 返回格式异常')); + } + } catch (_e) { + reject(new Error('API 返回解析失败')); + } + }); + }); + + req.on('error', (err) => { + reject(new Error('API 请求失败: ' + err.message)); + }); + + req.on('timeout', () => { + req.destroy(); + reject(new Error('API 请求超时')); + }); + + req.write(body); + req.end(); + }); +} + +// POST /api/ps/apikey/detect-models — 检测用户第三方 API 可用模型 +async function handleDetectModels(req, res) { + let body; + try { + body = await parseBody(req); + } catch (err) { + return jsonResponse(res, 400, { error: true, code: 'INVALID_BODY', message: err.message }); + } + + const { api_base, api_key } = body; + + if (!api_base || typeof api_base !== 'string') { + return jsonResponse(res, 400, { error: true, code: 'MISSING_API_BASE', message: '请输入 API Base URL' }); + } + + if (!api_key || typeof api_key !== 'string') { + return jsonResponse(res, 400, { error: true, code: 'MISSING_API_KEY', message: '请输入 API Key' }); + } + + // 防止 header injection + if (/[\r\n]/.test(api_key)) { + return jsonResponse(res, 400, { error: true, code: 'INVALID_API_KEY', message: 'API Key 格式无效(含非法字符)' }); + } + + // 校验 api_base 格式和协议(仅允许 http/https,防止 SSRF) + try { + const parsedUrl = new URL(api_base); + if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') { + return jsonResponse(res, 400, { error: true, code: 'INVALID_API_BASE', message: 'API Base URL 仅支持 http/https 协议' }); + } + } catch (_e) { + return jsonResponse(res, 400, { error: true, code: 'INVALID_API_BASE', message: 'API Base URL 格式无效,请输入完整 URL(如 https://api.openai.com)' }); + } + + // 检查缓存 + const cached = getCachedUserModels(api_base, api_key); + if (cached) { + return jsonResponse(res, 200, { error: false, models: cached, count: cached.length, cached: true }); + } + + try { + const models = await fetchUserModels(api_base, api_key, 15000); + + if (!models || models.length === 0) { + return jsonResponse(res, 404, { error: true, code: 'NO_MODELS', message: '未检测到可用模型,该 API 可能不支持模型枚举' }); + } + + setCachedUserModels(api_base, api_key, models); + + jsonResponse(res, 200, { error: false, models: models, count: models.length, cached: false }); + } catch (err) { + const code = err.code || 'DETECT_FAILED'; + const message = err.message || '模型检测失败'; + jsonResponse(res, 502, { error: true, code: code, message: message }); + } +} + +// POST /api/ps/apikey/chat — 通过用户 API Key 对话 +async function handleApiKeyChat(req, res) { + let body; + try { + body = await parseBody(req); + } catch (err) { + return jsonResponse(res, 400, { error: true, code: 'INVALID_BODY', message: err.message }); + } + + const { api_base, api_key, model, messages } = body; + + if (!api_base || !api_key || !model) { + return jsonResponse(res, 400, { error: true, code: 'MISSING_PARAMS', message: '缺少必要参数 (api_base, api_key, model)' }); + } + + if (/[\r\n]/.test(api_key)) { + return jsonResponse(res, 400, { error: true, code: 'INVALID_API_KEY', message: 'API Key 格式无效' }); + } + + if (!messages || !Array.isArray(messages) || messages.length === 0) { + return jsonResponse(res, 400, { error: true, code: 'MISSING_MESSAGES', message: '缺少消息内容' }); + } + + try { + const reply = await callUserApiChat({ + apiBase: api_base, + apiKey: api_key, + model: model, + messages: messages, + maxTokens: 2000, + temperature: 0.8, + timeoutMs: 60000 + }); + + jsonResponse(res, 200, { error: false, reply: reply, model: model }); + } catch (err) { + jsonResponse(res, 502, { error: true, code: 'CHAT_FAILED', message: err.message || '对话请求失败' }); + } +} + +// ═══════════════════════════════════════════════════════ +// Persona Studio 后端反向代理 +// 将 /api/ps/chat/*, /api/ps/auth/*, /api/ps/build/*, /api/ps/notify/* +// 转发到 persona-studio 后端服务(默认端口 3002) +// ═══════════════════════════════════════════════════════ +const PS_BACKEND_PORT = parseInt(process.env.PS_PORT || '3002', 10); +const PS_PROXY_PATHS = ['/api/ps/chat/', '/api/ps/auth/', '/api/ps/build/', '/api/ps/notify/']; + +function shouldProxyToPersonaStudio(pathname) { + return PS_PROXY_PATHS.some(prefix => pathname.startsWith(prefix)); +} + +function proxyToPersonaStudio(req, res, fullPath) { + return new Promise((resolve, reject) => { + const options = { + hostname: '127.0.0.1', + port: PS_BACKEND_PORT, + path: fullPath, + method: req.method, + headers: Object.assign({}, req.headers, { + host: '127.0.0.1:' + PS_BACKEND_PORT + }), + timeout: 60000 + }; + + const proxyReq = http.request(options, (proxyRes) => { + // Forward status and headers, but skip backend's CORS headers + // because the api-proxy already set CORS headers via setCorsHeaders() + // at the top of the request handler — forwarding both would cause duplicates + const headers = {}; + for (const [key, value] of Object.entries(proxyRes.headers)) { + if (!key.startsWith('access-control-')) { + headers[key] = value; + } + } + res.writeHead(proxyRes.statusCode, headers); + proxyRes.pipe(res); + proxyRes.on('end', resolve); + proxyRes.on('error', reject); + }); + + proxyReq.on('error', (err) => { + console.error('[代理→PS] persona-studio 后端连接失败:', err.message); + if (!res.headersSent) { + jsonResponse(res, 502, { + error: true, + code: 'PS_BACKEND_UNAVAILABLE', + message: 'Persona Studio 后端服务不可用,请稍后重试' + }); + } + resolve(); + }); + + proxyReq.on('timeout', () => { + proxyReq.destroy(); + if (!res.headersSent) { + jsonResponse(res, 504, { + error: true, + code: 'PS_BACKEND_TIMEOUT', + message: 'Persona Studio 后端响应超时' + }); + } + resolve(); + }); + + // Pipe the original request body to the proxy request + req.pipe(proxyReq); + }); +} + // ═══════════════════════════════════════════════════════ // HTTP 服务器 // ═══════════════════════════════════════════════════════ @@ -405,11 +803,17 @@ const server = http.createServer(async (req, res) => { handleGetModels(req, res); } else if (path === '/api/health' && req.method === 'GET') { handleHealth(req, res); + } else if (path === '/api/ps/apikey/detect-models' && req.method === 'POST') { + await handleDetectModels(req, res); + } else if (path === '/api/ps/apikey/chat' && req.method === 'POST') { + await handleApiKeyChat(req, res); + } else if (shouldProxyToPersonaStudio(path)) { + await proxyToPersonaStudio(req, res, url.pathname + url.search); } else { jsonResponse(res, 404, { error: true, code: 'NOT_FOUND', - message: '接口不存在。可用接口: POST /api/chat, GET /api/models, GET /api/health' + message: '接口不存在。可用接口: POST /api/chat, GET /api/models, GET /api/health, POST /api/ps/apikey/detect-models, POST /api/ps/apikey/chat, POST /api/ps/chat/message, GET /api/ps/chat/history, POST /api/ps/auth/login' }); } } catch (err) { @@ -449,9 +853,17 @@ server.listen(PORT, () => { console.log(' export YUNWU_API_KEY=sk-xxx'); } + console.log(`\n Persona Studio 后端代理: http://127.0.0.1:${PS_BACKEND_PORT}`); + console.log(' 代理路径: /api/ps/chat/*, /api/ps/auth/*, /api/ps/build/*, /api/ps/notify/*'); + console.log('\n 可用接口:'); - console.log(' POST /api/chat — 聊天代理(SSE 流式)'); - console.log(' GET /api/models — 列出可用模型'); - console.log(' GET /api/health — 健康检查'); + console.log(' POST /api/chat — 聊天代理(SSE 流式)'); + console.log(' GET /api/models — 列出可用模型'); + console.log(' GET /api/health — 健康检查'); + console.log(' POST /api/ps/apikey/detect-models — 用户 API Key 模型检测'); + console.log(' POST /api/ps/apikey/chat — 用户 API Key 对话'); + console.log(' POST /api/ps/chat/message — 知秋对话(→ PS后端)'); + console.log(' GET /api/ps/chat/history — 对话历史(→ PS后端)'); + console.log(' POST /api/ps/auth/login — 登录校验(→ PS后端)'); console.log(''); }); diff --git a/backend-integration/nginx-api-proxy.conf b/backend-integration/nginx-api-proxy.conf index cc36436b..2527dd67 100644 --- a/backend-integration/nginx-api-proxy.conf +++ b/backend-integration/nginx-api-proxy.conf @@ -5,7 +5,24 @@ # # 前端(GitHub Pages)→ Nginx → API 代理(Node.js :3721)→ 模型 API -# API 代理转发 +# Persona Studio API → persona-studio 后端端口 3002(含 CORS 跨域支持) +location /api/ps/ { + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; + add_header Access-Control-Allow-Headers "Content-Type, Authorization" always; + if ($request_method = OPTIONS) { + return 204; + } + proxy_pass http://127.0.0.1:3002/api/ps/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 90s; +} + +# API 代理转发(通用 AI 聊天) location /api/ { proxy_pass http://127.0.0.1:3721; proxy_http_version 1.1; @@ -24,3 +41,15 @@ location /api/ { proxy_read_timeout 90s; proxy_send_timeout 60s; } + +# Persona Studio WebSocket → 预览进度推送端口 3002 +location /ws/preview { + proxy_pass http://127.0.0.1:3002; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 86400; +} diff --git a/backend/bridge/README.md b/backend/bridge/README.md new file mode 100644 index 00000000..22c3ccb6 --- /dev/null +++ b/backend/bridge/README.md @@ -0,0 +1,42 @@ +# M-BRIDGE 中继桥接服务 +## 光湖四节点架构·神经连接层 +广播编号:BC-M-BRIDGE-001-FM +开发者:DEV-002 肥猫(HoloLake Human Bridge) +版本:v1.0 +技术栈:Node.js + Express + JSON持久化 + HTTP健康探测 + +### 模块定位 +光湖四节点(Notion/GitHub/guanghulab.com/钉钉)的神经连接层,负责所有节点间的数据流转、Webhook接收、状态聚合与健康检查。 + +### 核心功能 +1. 基础Express服务(端口3020),提供模块身份查询接口 +2. 接收GitHub/SYSLOG Webhook推送,实现事件路由与持久化 +3. 聚合所有注册节点状态,提供3秒超时健康探测 +4. 事件日志持久化与自动清理(30天),支持多条件查询 +5. 统一配置管理,敏感信息占位符隔离 + +### 配置文件 +- 配置路径:`config.json` +- 包含项:端口、日志路径、节点注册表、事件保留天数、敏感信息占位符 +- 注意:**敏感信息请勿推送到GitHub**,仅在本地配置真实值。 + +### 快速启动 +1. 进入模块目录:`cd guanghulab/backend/bridge` +2. 安装依赖:`npm install` +3. 启动服务:`node server.js` +4. 验证启动:访问 http://localhost:3020 查看模块身份信息 + +### 核心接口列表 +| 接口地址 | 请求方法 | 接口功能 | 验收项 | +|----------|----------|----------|--------| +| / | GET | 模块身份信息查询 | 0-2 | +| /bridge/webhook/github | POST | 接收GitHub Webhook推送 | 1-1 | +| /bridge/webhook/syslog | POST | 接收SYSLOG推送 | 1-3 | +| /bridge/status | GET | 桥接服务自身状态查询 | 2-1 | +| /bridge/nodes | GET | 所有注册节点健康状态查询 | 2-2 | +| /bridge/events | GET | 事件日志多条件查询 | 2-4 | + +### 仓库路径 +`backend/bridge/` +### 验收标准 +遵循BC-M-BRIDGE-001-FM广播全量验收总表,共20项验收要求。 diff --git a/backend/bridge/bridge-events.json b/backend/bridge/bridge-events.json new file mode 100644 index 00000000..9285b525 --- /dev/null +++ b/backend/bridge/bridge-events.json @@ -0,0 +1,31 @@ +[ + { + "event_id": "SL-20260309210612348", + "event_type": "syslog", + "source": "syslog_webhook", + "dev_id": "DEV-002", + "broadcast_id": "BC-M-BRIDGE-001-FM", + "status": "completed", + "summary": "??1????", + "timestamp": "2026-03-09 21:06:12", + "raw_payload": { + "dev_id": "DEV-002", + "broadcast_id": "BC-M-BRIDGE-001-FM", + "status": "completed", + "summary": "??1????" + } + }, + { + "event_id": "GH-20260309210821902", + "event_type": "github", + "sub_type": "pull_request", + "source": "github_webhook", + "summary": "PR #1: opened", + "timestamp": "2026-03-09 21:08:21", + "raw_payload": { + "number": 1, + "action": "opened", + "title": "??PR" + } + } +] diff --git a/backend/bridge/config.json b/backend/bridge/config.json new file mode 100644 index 00000000..36e2daed --- /dev/null +++ b/backend/bridge/config.json @@ -0,0 +1,25 @@ +{ + "module": "M-BRIDGE", + "version": "1.0", + "port": 3020, + "logPath": "./logs", + "eventLogFile": "./bridge-events.json", + "nodeTimeout": 3000, + "eventRetainDays": 30, + "nodes": [ + { + "name": "M14冷启动热身服务", + "url": "http://localhost:3000", + "type": "web" + }, + { + "name": "M18健康检查服务", + "url": "http://localhost:3010", + "type": "web" + } + ], + "secrets": { + "githubWebhookSecret": "GITHUB_WEBHOOK_SECRET_PLACEHOLDER", + "syslogAuthToken": "SYSLOG_AUTH_TOKEN_PLACEHOLDER" + } +} diff --git a/backend/bridge/node_modules/.package-lock.json b/backend/bridge/node_modules/.package-lock.json new file mode 100644 index 00000000..141eb37d --- /dev/null +++ b/backend/bridge/node_modules/.package-lock.json @@ -0,0 +1,799 @@ +{ + "name": "bridge", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + } +} diff --git a/backend/bridge/node_modules/accepts/HISTORY.md b/backend/bridge/node_modules/accepts/HISTORY.md new file mode 100644 index 00000000..627a81d0 --- /dev/null +++ b/backend/bridge/node_modules/accepts/HISTORY.md @@ -0,0 +1,250 @@ +2.0.0 / 2024-08-31 +================== + + * Drop node <18 support + * deps: mime-types@^3.0.0 + * deps: negotiator@^1.0.0 + +1.3.8 / 2022-02-02 +================== + + * deps: mime-types@~2.1.34 + - deps: mime-db@~1.51.0 + * deps: negotiator@0.6.3 + +1.3.7 / 2019-04-29 +================== + + * deps: negotiator@0.6.2 + - Fix sorting charset, encoding, and language with extra parameters + +1.3.6 / 2019-04-28 +================== + + * deps: mime-types@~2.1.24 + - deps: mime-db@~1.40.0 + +1.3.5 / 2018-02-28 +================== + + * deps: mime-types@~2.1.18 + - deps: mime-db@~1.33.0 + +1.3.4 / 2017-08-22 +================== + + * deps: mime-types@~2.1.16 + - deps: mime-db@~1.29.0 + +1.3.3 / 2016-05-02 +================== + + * deps: mime-types@~2.1.11 + - deps: mime-db@~1.23.0 + * deps: negotiator@0.6.1 + - perf: improve `Accept` parsing speed + - perf: improve `Accept-Charset` parsing speed + - perf: improve `Accept-Encoding` parsing speed + - perf: improve `Accept-Language` parsing speed + +1.3.2 / 2016-03-08 +================== + + * deps: mime-types@~2.1.10 + - Fix extension of `application/dash+xml` + - Update primary extension for `audio/mp4` + - deps: mime-db@~1.22.0 + +1.3.1 / 2016-01-19 +================== + + * deps: mime-types@~2.1.9 + - deps: mime-db@~1.21.0 + +1.3.0 / 2015-09-29 +================== + + * deps: mime-types@~2.1.7 + - deps: mime-db@~1.19.0 + * deps: negotiator@0.6.0 + - Fix including type extensions in parameters in `Accept` parsing + - Fix parsing `Accept` parameters with quoted equals + - Fix parsing `Accept` parameters with quoted semicolons + - Lazy-load modules from main entry point + - perf: delay type concatenation until needed + - perf: enable strict mode + - perf: hoist regular expressions + - perf: remove closures getting spec properties + - perf: remove a closure from media type parsing + - perf: remove property delete from media type parsing + +1.2.13 / 2015-09-06 +=================== + + * deps: mime-types@~2.1.6 + - deps: mime-db@~1.18.0 + +1.2.12 / 2015-07-30 +=================== + + * deps: mime-types@~2.1.4 + - deps: mime-db@~1.16.0 + +1.2.11 / 2015-07-16 +=================== + + * deps: mime-types@~2.1.3 + - deps: mime-db@~1.15.0 + +1.2.10 / 2015-07-01 +=================== + + * deps: mime-types@~2.1.2 + - deps: mime-db@~1.14.0 + +1.2.9 / 2015-06-08 +================== + + * deps: mime-types@~2.1.1 + - perf: fix deopt during mapping + +1.2.8 / 2015-06-07 +================== + + * deps: mime-types@~2.1.0 + - deps: mime-db@~1.13.0 + * perf: avoid argument reassignment & argument slice + * perf: avoid negotiator recursive construction + * perf: enable strict mode + * perf: remove unnecessary bitwise operator + +1.2.7 / 2015-05-10 +================== + + * deps: negotiator@0.5.3 + - Fix media type parameter matching to be case-insensitive + +1.2.6 / 2015-05-07 +================== + + * deps: mime-types@~2.0.11 + - deps: mime-db@~1.9.1 + * deps: negotiator@0.5.2 + - Fix comparing media types with quoted values + - Fix splitting media types with quoted commas + +1.2.5 / 2015-03-13 +================== + + * deps: mime-types@~2.0.10 + - deps: mime-db@~1.8.0 + +1.2.4 / 2015-02-14 +================== + + * Support Node.js 0.6 + * deps: mime-types@~2.0.9 + - deps: mime-db@~1.7.0 + * deps: negotiator@0.5.1 + - Fix preference sorting to be stable for long acceptable lists + +1.2.3 / 2015-01-31 +================== + + * deps: mime-types@~2.0.8 + - deps: mime-db@~1.6.0 + +1.2.2 / 2014-12-30 +================== + + * deps: mime-types@~2.0.7 + - deps: mime-db@~1.5.0 + +1.2.1 / 2014-12-30 +================== + + * deps: mime-types@~2.0.5 + - deps: mime-db@~1.3.1 + +1.2.0 / 2014-12-19 +================== + + * deps: negotiator@0.5.0 + - Fix list return order when large accepted list + - Fix missing identity encoding when q=0 exists + - Remove dynamic building of Negotiator class + +1.1.4 / 2014-12-10 +================== + + * deps: mime-types@~2.0.4 + - deps: mime-db@~1.3.0 + +1.1.3 / 2014-11-09 +================== + + * deps: mime-types@~2.0.3 + - deps: mime-db@~1.2.0 + +1.1.2 / 2014-10-14 +================== + + * deps: negotiator@0.4.9 + - Fix error when media type has invalid parameter + +1.1.1 / 2014-09-28 +================== + + * deps: mime-types@~2.0.2 + - deps: mime-db@~1.1.0 + * deps: negotiator@0.4.8 + - Fix all negotiations to be case-insensitive + - Stable sort preferences of same quality according to client order + +1.1.0 / 2014-09-02 +================== + + * update `mime-types` + +1.0.7 / 2014-07-04 +================== + + * Fix wrong type returned from `type` when match after unknown extension + +1.0.6 / 2014-06-24 +================== + + * deps: negotiator@0.4.7 + +1.0.5 / 2014-06-20 +================== + + * fix crash when unknown extension given + +1.0.4 / 2014-06-19 +================== + + * use `mime-types` + +1.0.3 / 2014-06-11 +================== + + * deps: negotiator@0.4.6 + - Order by specificity when quality is the same + +1.0.2 / 2014-05-29 +================== + + * Fix interpretation when header not in request + * deps: pin negotiator@0.4.5 + +1.0.1 / 2014-01-18 +================== + + * Identity encoding isn't always acceptable + * deps: negotiator@~0.4.0 + +1.0.0 / 2013-12-27 +================== + + * Genesis diff --git a/backend/bridge/node_modules/accepts/LICENSE b/backend/bridge/node_modules/accepts/LICENSE new file mode 100644 index 00000000..06166077 --- /dev/null +++ b/backend/bridge/node_modules/accepts/LICENSE @@ -0,0 +1,23 @@ +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/bridge/node_modules/accepts/README.md b/backend/bridge/node_modules/accepts/README.md new file mode 100644 index 00000000..f3f10c4a --- /dev/null +++ b/backend/bridge/node_modules/accepts/README.md @@ -0,0 +1,140 @@ +# accepts + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-url] +[![Node.js Version][node-version-image]][node-version-url] +[![Build Status][github-actions-ci-image]][github-actions-ci-url] +[![Test Coverage][coveralls-image]][coveralls-url] + +Higher level content negotiation based on [negotiator](https://www.npmjs.com/package/negotiator). +Extracted from [koa](https://www.npmjs.com/package/koa) for general use. + +In addition to negotiator, it allows: + +- Allows types as an array or arguments list, ie `(['text/html', 'application/json'])` + as well as `('text/html', 'application/json')`. +- Allows type shorthands such as `json`. +- Returns `false` when no types match +- Treats non-existent headers as `*` + +## Installation + +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). Installation is done using the +[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): + +```sh +$ npm install accepts +``` + +## API + +```js +var accepts = require('accepts') +``` + +### accepts(req) + +Create a new `Accepts` object for the given `req`. + +#### .charset(charsets) + +Return the first accepted charset. If nothing in `charsets` is accepted, +then `false` is returned. + +#### .charsets() + +Return the charsets that the request accepts, in the order of the client's +preference (most preferred first). + +#### .encoding(encodings) + +Return the first accepted encoding. If nothing in `encodings` is accepted, +then `false` is returned. + +#### .encodings() + +Return the encodings that the request accepts, in the order of the client's +preference (most preferred first). + +#### .language(languages) + +Return the first accepted language. If nothing in `languages` is accepted, +then `false` is returned. + +#### .languages() + +Return the languages that the request accepts, in the order of the client's +preference (most preferred first). + +#### .type(types) + +Return the first accepted type (and it is returned as the same text as what +appears in the `types` array). If nothing in `types` is accepted, then `false` +is returned. + +The `types` array can contain full MIME types or file extensions. Any value +that is not a full MIME type is passed to `require('mime-types').lookup`. + +#### .types() + +Return the types that the request accepts, in the order of the client's +preference (most preferred first). + +## Examples + +### Simple type negotiation + +This simple example shows how to use `accepts` to return a different typed +respond body based on what the client wants to accept. The server lists it's +preferences in order and will get back the best match between the client and +server. + +```js +var accepts = require('accepts') +var http = require('http') + +function app (req, res) { + var accept = accepts(req) + + // the order of this list is significant; should be server preferred order + switch (accept.type(['json', 'html'])) { + case 'json': + res.setHeader('Content-Type', 'application/json') + res.write('{"hello":"world!"}') + break + case 'html': + res.setHeader('Content-Type', 'text/html') + res.write('hello, world!') + break + default: + // the fallback is text/plain, so no need to specify it above + res.setHeader('Content-Type', 'text/plain') + res.write('hello, world!') + break + } + + res.end() +} + +http.createServer(app).listen(3000) +``` + +You can test this out with the cURL program: +```sh +curl -I -H'Accept: text/html' http://localhost:3000/ +``` + +## License + +[MIT](LICENSE) + +[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/accepts/master +[coveralls-url]: https://coveralls.io/r/jshttp/accepts?branch=master +[github-actions-ci-image]: https://badgen.net/github/checks/jshttp/accepts/master?label=ci +[github-actions-ci-url]: https://github.com/jshttp/accepts/actions/workflows/ci.yml +[node-version-image]: https://badgen.net/npm/node/accepts +[node-version-url]: https://nodejs.org/en/download +[npm-downloads-image]: https://badgen.net/npm/dm/accepts +[npm-url]: https://npmjs.org/package/accepts +[npm-version-image]: https://badgen.net/npm/v/accepts diff --git a/backend/bridge/node_modules/accepts/index.js b/backend/bridge/node_modules/accepts/index.js new file mode 100644 index 00000000..4f2840c0 --- /dev/null +++ b/backend/bridge/node_modules/accepts/index.js @@ -0,0 +1,238 @@ +/*! + * accepts + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var Negotiator = require('negotiator') +var mime = require('mime-types') + +/** + * Module exports. + * @public + */ + +module.exports = Accepts + +/** + * Create a new Accepts object for the given req. + * + * @param {object} req + * @public + */ + +function Accepts (req) { + if (!(this instanceof Accepts)) { + return new Accepts(req) + } + + this.headers = req.headers + this.negotiator = new Negotiator(req) +} + +/** + * Check if the given `type(s)` is acceptable, returning + * the best match when true, otherwise `undefined`, in which + * case you should respond with 406 "Not Acceptable". + * + * The `type` value may be a single mime type string + * such as "application/json", the extension name + * such as "json" or an array `["json", "html", "text/plain"]`. When a list + * or array is given the _best_ match, if any is returned. + * + * Examples: + * + * // Accept: text/html + * this.types('html'); + * // => "html" + * + * // Accept: text/*, application/json + * this.types('html'); + * // => "html" + * this.types('text/html'); + * // => "text/html" + * this.types('json', 'text'); + * // => "json" + * this.types('application/json'); + * // => "application/json" + * + * // Accept: text/*, application/json + * this.types('image/png'); + * this.types('png'); + * // => undefined + * + * // Accept: text/*;q=.5, application/json + * this.types(['html', 'json']); + * this.types('html', 'json'); + * // => "json" + * + * @param {String|Array} types... + * @return {String|Array|Boolean} + * @public + */ + +Accepts.prototype.type = +Accepts.prototype.types = function (types_) { + var types = types_ + + // support flattened arguments + if (types && !Array.isArray(types)) { + types = new Array(arguments.length) + for (var i = 0; i < types.length; i++) { + types[i] = arguments[i] + } + } + + // no types, return all requested types + if (!types || types.length === 0) { + return this.negotiator.mediaTypes() + } + + // no accept header, return first given type + if (!this.headers.accept) { + return types[0] + } + + var mimes = types.map(extToMime) + var accepts = this.negotiator.mediaTypes(mimes.filter(validMime)) + var first = accepts[0] + + return first + ? types[mimes.indexOf(first)] + : false +} + +/** + * Return accepted encodings or best fit based on `encodings`. + * + * Given `Accept-Encoding: gzip, deflate` + * an array sorted by quality is returned: + * + * ['gzip', 'deflate'] + * + * @param {String|Array} encodings... + * @return {String|Array} + * @public + */ + +Accepts.prototype.encoding = +Accepts.prototype.encodings = function (encodings_) { + var encodings = encodings_ + + // support flattened arguments + if (encodings && !Array.isArray(encodings)) { + encodings = new Array(arguments.length) + for (var i = 0; i < encodings.length; i++) { + encodings[i] = arguments[i] + } + } + + // no encodings, return all requested encodings + if (!encodings || encodings.length === 0) { + return this.negotiator.encodings() + } + + return this.negotiator.encodings(encodings)[0] || false +} + +/** + * Return accepted charsets or best fit based on `charsets`. + * + * Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5` + * an array sorted by quality is returned: + * + * ['utf-8', 'utf-7', 'iso-8859-1'] + * + * @param {String|Array} charsets... + * @return {String|Array} + * @public + */ + +Accepts.prototype.charset = +Accepts.prototype.charsets = function (charsets_) { + var charsets = charsets_ + + // support flattened arguments + if (charsets && !Array.isArray(charsets)) { + charsets = new Array(arguments.length) + for (var i = 0; i < charsets.length; i++) { + charsets[i] = arguments[i] + } + } + + // no charsets, return all requested charsets + if (!charsets || charsets.length === 0) { + return this.negotiator.charsets() + } + + return this.negotiator.charsets(charsets)[0] || false +} + +/** + * Return accepted languages or best fit based on `langs`. + * + * Given `Accept-Language: en;q=0.8, es, pt` + * an array sorted by quality is returned: + * + * ['es', 'pt', 'en'] + * + * @param {String|Array} langs... + * @return {Array|String} + * @public + */ + +Accepts.prototype.lang = +Accepts.prototype.langs = +Accepts.prototype.language = +Accepts.prototype.languages = function (languages_) { + var languages = languages_ + + // support flattened arguments + if (languages && !Array.isArray(languages)) { + languages = new Array(arguments.length) + for (var i = 0; i < languages.length; i++) { + languages[i] = arguments[i] + } + } + + // no languages, return all requested languages + if (!languages || languages.length === 0) { + return this.negotiator.languages() + } + + return this.negotiator.languages(languages)[0] || false +} + +/** + * Convert extnames to mime. + * + * @param {String} type + * @return {String} + * @private + */ + +function extToMime (type) { + return type.indexOf('/') === -1 + ? mime.lookup(type) + : type +} + +/** + * Check if mime is valid. + * + * @param {String} type + * @return {Boolean} + * @private + */ + +function validMime (type) { + return typeof type === 'string' +} diff --git a/backend/bridge/node_modules/accepts/package.json b/backend/bridge/node_modules/accepts/package.json new file mode 100644 index 00000000..b35b262d --- /dev/null +++ b/backend/bridge/node_modules/accepts/package.json @@ -0,0 +1,47 @@ +{ + "name": "accepts", + "description": "Higher-level content negotiation", + "version": "2.0.0", + "contributors": [ + "Douglas Christopher Wilson ", + "Jonathan Ong (http://jongleberry.com)" + ], + "license": "MIT", + "repository": "jshttp/accepts", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "devDependencies": { + "deep-equal": "1.0.1", + "eslint": "7.32.0", + "eslint-config-standard": "14.1.1", + "eslint-plugin-import": "2.25.4", + "eslint-plugin-markdown": "2.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "4.3.1", + "eslint-plugin-standard": "4.1.0", + "mocha": "9.2.0", + "nyc": "15.1.0" + }, + "files": [ + "LICENSE", + "HISTORY.md", + "index.js" + ], + "engines": { + "node": ">= 0.6" + }, + "scripts": { + "lint": "eslint .", + "test": "mocha --reporter spec --check-leaks --bail test/", + "test-ci": "nyc --reporter=lcov --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test" + }, + "keywords": [ + "content", + "negotiation", + "accept", + "accepts" + ] +} diff --git a/backend/bridge/node_modules/body-parser/LICENSE b/backend/bridge/node_modules/body-parser/LICENSE new file mode 100644 index 00000000..386b7b69 --- /dev/null +++ b/backend/bridge/node_modules/body-parser/LICENSE @@ -0,0 +1,23 @@ +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/bridge/node_modules/body-parser/README.md b/backend/bridge/node_modules/body-parser/README.md new file mode 100644 index 00000000..39d320f5 --- /dev/null +++ b/backend/bridge/node_modules/body-parser/README.md @@ -0,0 +1,494 @@ +# body-parser + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-url] +[![Build Status][ci-image]][ci-url] +[![Test Coverage][coveralls-image]][coveralls-url] +[![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer] + +Node.js body parsing middleware. + +Parse incoming request bodies in a middleware before your handlers, available +under the `req.body` property. + +**Note** As `req.body`'s shape is based on user-controlled input, all +properties and values in this object are untrusted and should be validated +before trusting. For example, `req.body.foo.toString()` may fail in multiple +ways, for example the `foo` property may not be there or may not be a string, +and `toString` may not be a function and instead a string or other user input. + +[Learn about the anatomy of an HTTP transaction in Node.js](https://nodejs.org/en/learn/http/anatomy-of-an-http-transaction). + +_This does not handle multipart bodies_, due to their complex and typically +large nature. For multipart bodies, you may be interested in the following +modules: + + * [busboy](https://www.npmjs.com/package/busboy#readme) and + [connect-busboy](https://www.npmjs.com/package/connect-busboy#readme) + * [multiparty](https://www.npmjs.com/package/multiparty#readme) and + [connect-multiparty](https://www.npmjs.com/package/connect-multiparty#readme) + * [formidable](https://www.npmjs.com/package/formidable#readme) + * [multer](https://www.npmjs.com/package/multer#readme) + +This module provides the following parsers: + + * [JSON body parser](#bodyparserjsonoptions) + * [Raw body parser](#bodyparserrawoptions) + * [Text body parser](#bodyparsertextoptions) + * [URL-encoded form body parser](#bodyparserurlencodedoptions) + +Other body parsers you might be interested in: + +- [body](https://www.npmjs.com/package/body#readme) +- [co-body](https://www.npmjs.com/package/co-body#readme) + +## Installation + +```sh +$ npm install body-parser +``` + +## API + +```js +const bodyParser = require('body-parser') +``` + +The `bodyParser` object exposes various factories to create middlewares. All +middlewares will populate the `req.body` property with the parsed body when +the `Content-Type` request header matches the `type` option. + +The various errors returned by this module are described in the +[errors section](#errors). + +### bodyParser.json([options]) + +Returns middleware that only parses `json` and only looks at requests where +the `Content-Type` header matches the `type` option. This parser accepts any +Unicode encoding of the body and supports automatic inflation of `gzip`, +`br` (brotli) and `deflate` encodings. + +A new `body` object containing the parsed data is populated on the `request` +object after the middleware (i.e. `req.body`). + +#### Options + +The `json` function takes an optional `options` object that may contain any of +the following keys: + +##### defaultCharset + +Specify the default character set for the json content if the charset is not +specified in the `Content-Type` header of the request. Defaults to `utf-8`. + +##### inflate + +When set to `true`, then deflated (compressed) bodies will be inflated; when +`false`, deflated bodies are rejected. Defaults to `true`. + +##### limit + +Controls the maximum request body size. If this is a number, then the value +specifies the number of bytes; if it is a string, the value is passed to the +[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults +to `'100kb'`. + +##### reviver + +The `reviver` option is passed directly to `JSON.parse` as the second +argument. You can find more information on this argument +[in the MDN documentation about JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Example.3A_Using_the_reviver_parameter). + +##### strict + +When set to `true`, will only accept arrays and objects; when `false` will +accept anything `JSON.parse` accepts. Defaults to `true`. + +##### type + +The `type` option is used to determine what media type the middleware will +parse. This option can be a string, array of strings, or a function. If not a +function, `type` option is passed directly to the +[type-is](https://www.npmjs.com/package/type-is#readme) library and this can +be an extension name (like `json`), a mime type (like `application/json`), or +a mime type with a wildcard (like `*/*` or `*/json`). If a function, the `type` +option is called as `fn(req)` and the request is parsed if it returns a truthy +value. Defaults to `application/json`. + +##### verify + +The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`, +where `buf` is a `Buffer` of the raw request body and `encoding` is the +encoding of the request. The parsing can be aborted by throwing an error. + +### bodyParser.raw([options]) + +Returns middleware that parses all bodies as a `Buffer` and only looks at +requests where the `Content-Type` header matches the `type` option. This +parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate` +encodings. + +A new `body` object containing the parsed data is populated on the `request` +object after the middleware (i.e. `req.body`). This will be a `Buffer` object +of the body. + +#### Options + +The `raw` function takes an optional `options` object that may contain any of +the following keys: + +##### inflate + +When set to `true`, then deflated (compressed) bodies will be inflated; when +`false`, deflated bodies are rejected. Defaults to `true`. + +##### limit + +Controls the maximum request body size. If this is a number, then the value +specifies the number of bytes; if it is a string, the value is passed to the +[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults +to `'100kb'`. + +##### type + +The `type` option is used to determine what media type the middleware will +parse. This option can be a string, array of strings, or a function. +If not a function, `type` option is passed directly to the +[type-is](https://www.npmjs.com/package/type-is#readme) library and this +can be an extension name (like `bin`), a mime type (like +`application/octet-stream`), or a mime type with a wildcard (like `*/*` or +`application/*`). If a function, the `type` option is called as `fn(req)` +and the request is parsed if it returns a truthy value. Defaults to +`application/octet-stream`. + +##### verify + +The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`, +where `buf` is a `Buffer` of the raw request body and `encoding` is the +encoding of the request. The parsing can be aborted by throwing an error. + +### bodyParser.text([options]) + +Returns middleware that parses all bodies as a string and only looks at +requests where the `Content-Type` header matches the `type` option. This +parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate` +encodings. + +A new `body` string containing the parsed data is populated on the `request` +object after the middleware (i.e. `req.body`). This will be a string of the +body. + +#### Options + +The `text` function takes an optional `options` object that may contain any of +the following keys: + +##### defaultCharset + +Specify the default character set for the text content if the charset is not +specified in the `Content-Type` header of the request. Defaults to `utf-8`. + +##### inflate + +When set to `true`, then deflated (compressed) bodies will be inflated; when +`false`, deflated bodies are rejected. Defaults to `true`. + +##### limit + +Controls the maximum request body size. If this is a number, then the value +specifies the number of bytes; if it is a string, the value is passed to the +[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults +to `'100kb'`. + +##### type + +The `type` option is used to determine what media type the middleware will +parse. This option can be a string, array of strings, or a function. If not +a function, `type` option is passed directly to the +[type-is](https://www.npmjs.com/package/type-is#readme) library and this can +be an extension name (like `txt`), a mime type (like `text/plain`), or a mime +type with a wildcard (like `*/*` or `text/*`). If a function, the `type` +option is called as `fn(req)` and the request is parsed if it returns a +truthy value. Defaults to `text/plain`. + +##### verify + +The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`, +where `buf` is a `Buffer` of the raw request body and `encoding` is the +encoding of the request. The parsing can be aborted by throwing an error. + +### bodyParser.urlencoded([options]) + +Returns middleware that only parses `urlencoded` bodies and only looks at +requests where the `Content-Type` header matches the `type` option. This +parser accepts only UTF-8 and ISO-8859-1 encodings of the body and supports +automatic inflation of `gzip`, `br` (brotli) and `deflate` encodings. + +A new `body` object containing the parsed data is populated on the `request` +object after the middleware (i.e. `req.body`). This object will contain +key-value pairs, where the value can be a string or array (when `extended` is +`false`), or any type (when `extended` is `true`). + +#### Options + +The `urlencoded` function takes an optional `options` object that may contain +any of the following keys: + +##### extended + +The "extended" syntax allows for rich objects and arrays to be encoded into the +URL-encoded format, allowing for a JSON-like experience with URL-encoded. For +more information, please [see the qs +library](https://www.npmjs.com/package/qs#readme). + +Defaults to `false`. + +##### inflate + +When set to `true`, then deflated (compressed) bodies will be inflated; when +`false`, deflated bodies are rejected. Defaults to `true`. + +##### limit + +Controls the maximum request body size. If this is a number, then the value +specifies the number of bytes; if it is a string, the value is passed to the +[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults +to `'100kb'`. + +##### parameterLimit + +The `parameterLimit` option controls the maximum number of parameters that +are allowed in the URL-encoded data. If a request contains more parameters +than this value, a 413 will be returned to the client. Defaults to `1000`. + +##### type + +The `type` option is used to determine what media type the middleware will +parse. This option can be a string, array of strings, or a function. If not +a function, `type` option is passed directly to the +[type-is](https://www.npmjs.com/package/type-is#readme) library and this can +be an extension name (like `urlencoded`), a mime type (like +`application/x-www-form-urlencoded`), or a mime type with a wildcard (like +`*/x-www-form-urlencoded`). If a function, the `type` option is called as +`fn(req)` and the request is parsed if it returns a truthy value. Defaults +to `application/x-www-form-urlencoded`. + +##### verify + +The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`, +where `buf` is a `Buffer` of the raw request body and `encoding` is the +encoding of the request. The parsing can be aborted by throwing an error. + +##### defaultCharset + +The default charset to parse as, if not specified in content-type. Must be +either `utf-8` or `iso-8859-1`. Defaults to `utf-8`. + +##### charsetSentinel + +Whether to let the value of the `utf8` parameter take precedence as the charset +selector. It requires the form to contain a parameter named `utf8` with a value +of `✓`. Defaults to `false`. + +##### interpretNumericEntities + +Whether to decode numeric entities such as `☺` when parsing an iso-8859-1 +form. Defaults to `false`. + + +##### depth + +The `depth` option is used to configure the maximum depth of the `qs` library when `extended` is `true`. This allows you to limit the amount of keys that are parsed and can be useful to prevent certain types of abuse. Defaults to `32`. It is recommended to keep this value as low as possible. + +## Errors + +The middlewares provided by this module create errors using the +[`http-errors` module](https://www.npmjs.com/package/http-errors). The errors +will typically have a `status`/`statusCode` property that contains the suggested +HTTP response code, an `expose` property to determine if the `message` property +should be displayed to the client, a `type` property to determine the type of +error without matching against the `message`, and a `body` property containing +the read body, if available. + +The following are the common errors created, though any error can come through +for various reasons. + +### content encoding unsupported + +This error will occur when the request had a `Content-Encoding` header that +contained an encoding but the "inflation" option was set to `false`. The +`status` property is set to `415`, the `type` property is set to +`'encoding.unsupported'`, and the `charset` property will be set to the +encoding that is unsupported. + +### entity parse failed + +This error will occur when the request contained an entity that could not be +parsed by the middleware. The `status` property is set to `400`, the `type` +property is set to `'entity.parse.failed'`, and the `body` property is set to +the entity value that failed parsing. + +### entity verify failed + +This error will occur when the request contained an entity that could not be +failed verification by the defined `verify` option. The `status` property is +set to `403`, the `type` property is set to `'entity.verify.failed'`, and the +`body` property is set to the entity value that failed verification. + +### request aborted + +This error will occur when the request is aborted by the client before reading +the body has finished. The `received` property will be set to the number of +bytes received before the request was aborted and the `expected` property is +set to the number of expected bytes. The `status` property is set to `400` +and `type` property is set to `'request.aborted'`. + +### request entity too large + +This error will occur when the request body's size is larger than the "limit" +option. The `limit` property will be set to the byte limit and the `length` +property will be set to the request body's length. The `status` property is +set to `413` and the `type` property is set to `'entity.too.large'`. + +### request size did not match content length + +This error will occur when the request's length did not match the length from +the `Content-Length` header. This typically occurs when the request is malformed, +typically when the `Content-Length` header was calculated based on characters +instead of bytes. The `status` property is set to `400` and the `type` property +is set to `'request.size.invalid'`. + +### stream encoding should not be set + +This error will occur when something called the `req.setEncoding` method prior +to this middleware. This module operates directly on bytes only and you cannot +call `req.setEncoding` when using this module. The `status` property is set to +`500` and the `type` property is set to `'stream.encoding.set'`. + +### stream is not readable + +This error will occur when the request is no longer readable when this middleware +attempts to read it. This typically means something other than a middleware from +this module read the request body already and the middleware was also configured to +read the same request. The `status` property is set to `500` and the `type` +property is set to `'stream.not.readable'`. + +### too many parameters + +This error will occur when the content of the request exceeds the configured +`parameterLimit` for the `urlencoded` parser. The `status` property is set to +`413` and the `type` property is set to `'parameters.too.many'`. + +### unsupported charset "BOGUS" + +This error will occur when the request had a charset parameter in the +`Content-Type` header, but the `iconv-lite` module does not support it OR the +parser does not support it. The charset is contained in the message as well +as in the `charset` property. The `status` property is set to `415`, the +`type` property is set to `'charset.unsupported'`, and the `charset` property +is set to the charset that is unsupported. + +### unsupported content encoding "bogus" + +This error will occur when the request had a `Content-Encoding` header that +contained an unsupported encoding. The encoding is contained in the message +as well as in the `encoding` property. The `status` property is set to `415`, +the `type` property is set to `'encoding.unsupported'`, and the `encoding` +property is set to the encoding that is unsupported. + +### The input exceeded the depth + +This error occurs when using `bodyParser.urlencoded` with the `extended` property set to `true` and the input exceeds the configured `depth` option. The `status` property is set to `400`. It is recommended to review the `depth` option and evaluate if it requires a higher value. When the `depth` option is set to `32` (default value), the error will not be thrown. + +## Examples + +### Express/Connect top-level generic + +This example demonstrates adding a generic JSON and URL-encoded parser as a +top-level middleware, which will parse the bodies of all incoming requests. +This is the simplest setup. + +```js +const express = require('express') +const bodyParser = require('body-parser') + +const app = express() + +// parse application/x-www-form-urlencoded +app.use(bodyParser.urlencoded()) + +// parse application/json +app.use(bodyParser.json()) + +app.use(function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.write('you posted:\n') + res.end(String(JSON.stringify(req.body, null, 2))) +}) +``` + +### Express route-specific + +This example demonstrates adding body parsers specifically to the routes that +need them. In general, this is the most recommended way to use body-parser with +Express. + +```js +const express = require('express') +const bodyParser = require('body-parser') + +const app = express() + +// create application/json parser +const jsonParser = bodyParser.json() + +// create application/x-www-form-urlencoded parser +const urlencodedParser = bodyParser.urlencoded() + +// POST /login gets urlencoded bodies +app.post('/login', urlencodedParser, function (req, res) { + if (!req.body || !req.body.username) res.sendStatus(400) + res.send('welcome, ' + req.body.username) +}) + +// POST /api/users gets JSON bodies +app.post('/api/users', jsonParser, function (req, res) { + if (!req.body) res.sendStatus(400) + // create user in req.body +}) +``` + +### Change accepted type for parsers + +All the parsers accept a `type` option which allows you to change the +`Content-Type` that the middleware will parse. + +```js +const express = require('express') +const bodyParser = require('body-parser') + +const app = express() + +// parse various different custom JSON types as JSON +app.use(bodyParser.json({ type: 'application/*+json' })) + +// parse some custom thing into a Buffer +app.use(bodyParser.raw({ type: 'application/vnd.custom-type' })) + +// parse an HTML body into a string +app.use(bodyParser.text({ type: 'text/html' })) +``` + +## License + +[MIT](LICENSE) + +[ci-image]: https://img.shields.io/github/actions/workflow/status/expressjs/body-parser/ci.yml?branch=master&label=ci +[ci-url]: https://github.com/expressjs/body-parser/actions/workflows/ci.yml +[coveralls-image]: https://img.shields.io/coverallsCoverage/github/expressjs/body-parser?branch=master +[coveralls-url]: https://coveralls.io/r/expressjs/body-parser?branch=master +[npm-downloads-image]: https://img.shields.io/npm/dm/body-parser +[npm-url]: https://npmjs.com/package/body-parser +[npm-version-image]: https://img.shields.io/npm/v/body-parser +[ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/body-parser/badge +[ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/body-parser diff --git a/backend/bridge/node_modules/body-parser/index.js b/backend/bridge/node_modules/body-parser/index.js new file mode 100644 index 00000000..013ce5c4 --- /dev/null +++ b/backend/bridge/node_modules/body-parser/index.js @@ -0,0 +1,71 @@ +/*! + * body-parser + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * @typedef {Object} Parsers + * @property {Function} json JSON parser + * @property {Function} raw Raw parser + * @property {Function} text Text parser + * @property {Function} urlencoded URL-encoded parser + */ + +/** + * Module exports. + * @type {Function & Parsers} + */ +exports = module.exports = bodyParser + +/** + * JSON parser. + * @public + */ +Object.defineProperty(exports, 'json', { + configurable: true, + enumerable: true, + get: () => require('./lib/types/json') +}) + +/** + * Raw parser. + * @public + */ +Object.defineProperty(exports, 'raw', { + configurable: true, + enumerable: true, + get: () => require('./lib/types/raw') +}) + +/** + * Text parser. + * @public + */ +Object.defineProperty(exports, 'text', { + configurable: true, + enumerable: true, + get: () => require('./lib/types/text') +}) + +/** + * URL-encoded parser. + * @public + */ +Object.defineProperty(exports, 'urlencoded', { + configurable: true, + enumerable: true, + get: () => require('./lib/types/urlencoded') +}) + +/** + * Create a middleware to parse json and urlencoded bodies. + * + * @deprecated + * @public + */ +function bodyParser () { + throw new Error('The bodyParser() generic has been split into individual middleware to use instead.') +} diff --git a/backend/bridge/node_modules/body-parser/lib/read.js b/backend/bridge/node_modules/body-parser/lib/read.js new file mode 100644 index 00000000..d1f3f480 --- /dev/null +++ b/backend/bridge/node_modules/body-parser/lib/read.js @@ -0,0 +1,247 @@ +/*! + * body-parser + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var createError = require('http-errors') +var getBody = require('raw-body') +var iconv = require('iconv-lite') +var onFinished = require('on-finished') +var zlib = require('node:zlib') +var hasBody = require('type-is').hasBody +var { getCharset } = require('./utils') + +/** + * Module exports. + */ + +module.exports = read + +/** + * Read a request into a buffer and parse. + * + * @param {Object} req + * @param {Object} res + * @param {Function} next + * @param {Function} parse + * @param {Function} debug + * @param {Object} options + * @private + */ +function read (req, res, next, parse, debug, options) { + if (onFinished.isFinished(req)) { + debug('body already parsed') + next() + return + } + + if (!('body' in req)) { + req.body = undefined + } + + // skip requests without bodies + if (!hasBody(req)) { + debug('skip empty body') + next() + return + } + + debug('content-type %j', req.headers['content-type']) + + // determine if request should be parsed + if (!options.shouldParse(req)) { + debug('skip parsing') + next() + return + } + + var encoding = null + if (options?.skipCharset !== true) { + encoding = getCharset(req) || options.defaultCharset + + // validate charset + if (!!options?.isValidCharset && !options.isValidCharset(encoding)) { + debug('invalid charset') + next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', { + charset: encoding, + type: 'charset.unsupported' + })) + return + } + } + + var length + var opts = options + var stream + + // read options + var verify = opts.verify + + try { + // get the content stream + stream = contentstream(req, debug, opts.inflate) + length = stream.length + stream.length = undefined + } catch (err) { + return next(err) + } + + // set raw-body options + opts.length = length + opts.encoding = verify + ? null + : encoding + + // assert charset is supported + if (opts.encoding === null && encoding !== null && !iconv.encodingExists(encoding)) { + return next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', { + charset: encoding.toLowerCase(), + type: 'charset.unsupported' + })) + } + + // read body + debug('read body') + getBody(stream, opts, function (error, body) { + if (error) { + var _error + + if (error.type === 'encoding.unsupported') { + // echo back charset + _error = createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', { + charset: encoding.toLowerCase(), + type: 'charset.unsupported' + }) + } else { + // set status code on error + _error = createError(400, error) + } + + // unpipe from stream and destroy + if (stream !== req) { + req.unpipe() + stream.destroy() + } + + // read off entire request + dump(req, function onfinished () { + next(createError(400, _error)) + }) + return + } + + // verify + if (verify) { + try { + debug('verify body') + verify(req, res, body, encoding) + } catch (err) { + next(createError(403, err, { + body: body, + type: err.type || 'entity.verify.failed' + })) + return + } + } + + // parse + var str = body + try { + debug('parse body') + str = typeof body !== 'string' && encoding !== null + ? iconv.decode(body, encoding) + : body + req.body = parse(str, encoding) + } catch (err) { + next(createError(400, err, { + body: str, + type: err.type || 'entity.parse.failed' + })) + return + } + + next() + }) +} + +/** + * Get the content stream of the request. + * + * @param {Object} req + * @param {Function} debug + * @param {boolean} inflate + * @returns {Object} + * @private + */ +function contentstream (req, debug, inflate) { + var encoding = (req.headers['content-encoding'] || 'identity').toLowerCase() + var length = req.headers['content-length'] + + debug('content-encoding "%s"', encoding) + + if (inflate === false && encoding !== 'identity') { + throw createError(415, 'content encoding unsupported', { + encoding: encoding, + type: 'encoding.unsupported' + }) + } + + if (encoding === 'identity') { + req.length = length + return req + } + + var stream = createDecompressionStream(encoding, debug) + req.pipe(stream) + return stream +} + +/** + * Create a decompression stream for the given encoding. + * @param {string} encoding + * @param {Function} debug + * @returns {Object} + * @private + */ +function createDecompressionStream (encoding, debug) { + switch (encoding) { + case 'deflate': + debug('inflate body') + return zlib.createInflate() + case 'gzip': + debug('gunzip body') + return zlib.createGunzip() + case 'br': + debug('brotli decompress body') + return zlib.createBrotliDecompress() + default: + throw createError(415, 'unsupported content encoding "' + encoding + '"', { + encoding: encoding, + type: 'encoding.unsupported' + }) + } +} + +/** + * Dump the contents of a request. + * + * @param {Object} req + * @param {Function} callback + * @private + */ +function dump (req, callback) { + if (onFinished.isFinished(req)) { + callback(null) + } else { + onFinished(req, callback) + req.resume() + } +} diff --git a/backend/bridge/node_modules/body-parser/lib/types/json.js b/backend/bridge/node_modules/body-parser/lib/types/json.js new file mode 100644 index 00000000..23c73577 --- /dev/null +++ b/backend/bridge/node_modules/body-parser/lib/types/json.js @@ -0,0 +1,158 @@ +/*! + * body-parser + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var debug = require('debug')('body-parser:json') +var read = require('../read') +var { normalizeOptions } = require('../utils') + +/** + * Module exports. + */ + +module.exports = json + +/** + * RegExp to match the first non-space in a string. + * + * Allowed whitespace is defined in RFC 7159: + * + * ws = *( + * %x20 / ; Space + * %x09 / ; Horizontal tab + * %x0A / ; Line feed or New line + * %x0D ) ; Carriage return + */ +var FIRST_CHAR_REGEXP = /^[\x20\x09\x0a\x0d]*([^\x20\x09\x0a\x0d])/ // eslint-disable-line no-control-regex + +var JSON_SYNTAX_CHAR = '#' +var JSON_SYNTAX_REGEXP = /#+/g + +/** + * Create a middleware to parse JSON bodies. + * + * @param {Object} [options] + * @returns {Function} + * @public + */ +function json (options) { + const normalizedOptions = normalizeOptions(options, 'application/json') + + var reviver = options?.reviver + var strict = options?.strict !== false + + function parse (body) { + if (body.length === 0) { + // special-case empty json body, as it's a common client-side mistake + // TODO: maybe make this configurable or part of "strict" option + return {} + } + + if (strict) { + var first = firstchar(body) + + if (first !== '{' && first !== '[') { + debug('strict violation') + throw createStrictSyntaxError(body, first) + } + } + + try { + debug('parse json') + return JSON.parse(body, reviver) + } catch (e) { + throw normalizeJsonSyntaxError(e, { + message: e.message, + stack: e.stack + }) + } + } + + const readOptions = { + ...normalizedOptions, + // assert charset per RFC 7159 sec 8.1 + isValidCharset: (charset) => charset.slice(0, 4) === 'utf-' + } + + return function jsonParser (req, res, next) { + read(req, res, next, parse, debug, readOptions) + } +} + +/** + * Create strict violation syntax error matching native error. + * + * @param {string} str + * @param {string} char + * @returns {Error} + * @private + */ +function createStrictSyntaxError (str, char) { + var index = str.indexOf(char) + var partial = '' + + if (index !== -1) { + partial = str.substring(0, index) + JSON_SYNTAX_CHAR.repeat(str.length - index) + } + + try { + JSON.parse(partial); /* istanbul ignore next */ throw new SyntaxError('strict violation') + } catch (e) { + return normalizeJsonSyntaxError(e, { + message: e.message.replace(JSON_SYNTAX_REGEXP, function (placeholder) { + return str.substring(index, index + placeholder.length) + }), + stack: e.stack + }) + } +} + +/** + * Get the first non-whitespace character in a string. + * + * @param {string} str + * @returns {string|undefined} + * @private + */ +function firstchar (str) { + var match = FIRST_CHAR_REGEXP.exec(str) + + return match + ? match[1] + : undefined +} + +/** + * Normalize a SyntaxError for JSON.parse. + * + * @param {SyntaxError} error + * @param {Object} obj + * @returns {SyntaxError} + * @private + */ +function normalizeJsonSyntaxError (error, obj) { + var keys = Object.getOwnPropertyNames(error) + + for (var i = 0; i < keys.length; i++) { + var key = keys[i] + if (key !== 'stack' && key !== 'message') { + delete error[key] + } + } + + // replace stack before message for Node.js 0.10 and below + error.stack = obj.stack.replace(error.message, obj.message) + error.message = obj.message + + return error +} diff --git a/backend/bridge/node_modules/body-parser/lib/types/raw.js b/backend/bridge/node_modules/body-parser/lib/types/raw.js new file mode 100644 index 00000000..ad4854dd --- /dev/null +++ b/backend/bridge/node_modules/body-parser/lib/types/raw.js @@ -0,0 +1,42 @@ +/*! + * body-parser + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + */ + +var debug = require('debug')('body-parser:raw') +var read = require('../read') +var { normalizeOptions, passthrough } = require('../utils') + +/** + * Module exports. + */ + +module.exports = raw + +/** + * Create a middleware to parse raw bodies. + * + * @param {Object} [options] + * @returns {Function} + * @public + */ +function raw (options) { + const normalizedOptions = normalizeOptions(options, 'application/octet-stream') + + const readOptions = { + ...normalizedOptions, + // Skip charset validation and parse the body as is + skipCharset: true + } + + return function rawParser (req, res, next) { + read(req, res, next, passthrough, debug, readOptions) + } +} diff --git a/backend/bridge/node_modules/body-parser/lib/types/text.js b/backend/bridge/node_modules/body-parser/lib/types/text.js new file mode 100644 index 00000000..9df73be9 --- /dev/null +++ b/backend/bridge/node_modules/body-parser/lib/types/text.js @@ -0,0 +1,36 @@ +/*! + * body-parser + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + */ + +var debug = require('debug')('body-parser:text') +var read = require('../read') +var { normalizeOptions, passthrough } = require('../utils') + +/** + * Module exports. + */ + +module.exports = text + +/** + * Create a middleware to parse text bodies. + * + * @param {Object} [options] + * @returns {Function} + * @public + */ +function text (options) { + const normalizedOptions = normalizeOptions(options, 'text/plain') + + return function textParser (req, res, next) { + read(req, res, next, passthrough, debug, normalizedOptions) + } +} diff --git a/backend/bridge/node_modules/body-parser/lib/types/urlencoded.js b/backend/bridge/node_modules/body-parser/lib/types/urlencoded.js new file mode 100644 index 00000000..4a3227c1 --- /dev/null +++ b/backend/bridge/node_modules/body-parser/lib/types/urlencoded.js @@ -0,0 +1,142 @@ +/*! + * body-parser + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var createError = require('http-errors') +var debug = require('debug')('body-parser:urlencoded') +var read = require('../read') +var qs = require('qs') +var { normalizeOptions } = require('../utils') + +/** + * Module exports. + */ + +module.exports = urlencoded + +/** + * Create a middleware to parse urlencoded bodies. + * + * @param {Object} [options] + * @returns {Function} + * @public + */ +function urlencoded (options) { + const normalizedOptions = normalizeOptions(options, 'application/x-www-form-urlencoded') + + if (normalizedOptions.defaultCharset !== 'utf-8' && normalizedOptions.defaultCharset !== 'iso-8859-1') { + throw new TypeError('option defaultCharset must be either utf-8 or iso-8859-1') + } + + // create the appropriate query parser + var queryparse = createQueryParser(options) + + function parse (body, encoding) { + return body.length + ? queryparse(body, encoding) + : {} + } + + const readOptions = { + ...normalizedOptions, + // assert charset + isValidCharset: (charset) => charset === 'utf-8' || charset === 'iso-8859-1' + } + + return function urlencodedParser (req, res, next) { + read(req, res, next, parse, debug, readOptions) + } +} + +/** + * Get the extended query parser. + * + * @param {Object} options + * @returns {Function} + * @private + */ +function createQueryParser (options) { + var extended = Boolean(options?.extended) + var parameterLimit = options?.parameterLimit !== undefined + ? options?.parameterLimit + : 1000 + var charsetSentinel = options?.charsetSentinel + var interpretNumericEntities = options?.interpretNumericEntities + var depth = extended ? (options?.depth !== undefined ? options?.depth : 32) : 0 + + if (isNaN(parameterLimit) || parameterLimit < 1) { + throw new TypeError('option parameterLimit must be a positive number') + } + + if (isNaN(depth) || depth < 0) { + throw new TypeError('option depth must be a zero or a positive number') + } + + if (isFinite(parameterLimit)) { + parameterLimit = parameterLimit | 0 + } + + return function queryparse (body, encoding) { + var paramCount = parameterCount(body, parameterLimit) + + if (paramCount === undefined) { + debug('too many parameters') + throw createError(413, 'too many parameters', { + type: 'parameters.too.many' + }) + } + + var arrayLimit = extended ? Math.max(100, paramCount) : paramCount + + debug('parse ' + (extended ? 'extended ' : '') + 'urlencoding') + try { + return qs.parse(body, { + allowPrototypes: true, + arrayLimit: arrayLimit, + depth: depth, + charsetSentinel: charsetSentinel, + interpretNumericEntities: interpretNumericEntities, + charset: encoding, + parameterLimit: parameterLimit, + strictDepth: true + }) + } catch (err) { + if (err instanceof RangeError) { + throw createError(400, 'The input exceeded the depth', { + type: 'querystring.parse.rangeError' + }) + } else { + throw err + } + } + } +} + +/** + * Count the number of parameters, stopping once limit reached + * + * @param {string} body + * @param {number} limit + * @returns {number|undefined} Returns undefined if limit exceeded + * @private + */ +function parameterCount (body, limit) { + let count = 0 + let index = -1 + do { + count++ + if (count > limit) return undefined // Early exit if limit exceeded + index = body.indexOf('&', index + 1) + } while (index !== -1) + return count +} diff --git a/backend/bridge/node_modules/body-parser/lib/utils.js b/backend/bridge/node_modules/body-parser/lib/utils.js new file mode 100644 index 00000000..e0bf9741 --- /dev/null +++ b/backend/bridge/node_modules/body-parser/lib/utils.js @@ -0,0 +1,98 @@ +'use strict' + +/** + * Module dependencies. + */ + +var bytes = require('bytes') +var contentType = require('content-type') +var typeis = require('type-is') + +/** + * Module exports. + */ +module.exports = { + getCharset, + normalizeOptions, + passthrough +} + +/** + * Get the charset of a request. + * + * @param {Object} req + * @returns {string | undefined} + * @private + */ +function getCharset (req) { + try { + return (contentType.parse(req).parameters.charset || '').toLowerCase() + } catch { + return undefined + } +} + +/** + * Get the simple type checker. + * + * @param {string | string[]} type + * @returns {Function} + * @private + */ +function typeChecker (type) { + return function checkType (req) { + return Boolean(typeis(req, type)) + } +} + +/** + * Normalizes the common options for all parsers. + * + * @param {Object} options options to normalize + * @param {string | string[] | Function} defaultType default content type(s) or a function to determine it + * @returns {Object} + * @private + */ +function normalizeOptions (options, defaultType) { + if (!defaultType) { + // Parsers must define a default content type + throw new TypeError('defaultType must be provided') + } + + var inflate = options?.inflate !== false + var limit = typeof options?.limit !== 'number' + ? bytes.parse(options?.limit || '100kb') + : options?.limit + var type = options?.type || defaultType + var verify = options?.verify || false + var defaultCharset = options?.defaultCharset || 'utf-8' + + if (verify !== false && typeof verify !== 'function') { + throw new TypeError('option verify must be function') + } + + // create the appropriate type checking function + var shouldParse = typeof type !== 'function' + ? typeChecker(type) + : type + + return { + inflate, + limit, + verify, + defaultCharset, + shouldParse + } +} + +/** + * Passthrough function that returns input unchanged. + * Used by parsers that don't need to transform the data. + * + * @param {*} value + * @returns {*} + * @private + */ +function passthrough (value) { + return value +} diff --git a/backend/bridge/node_modules/body-parser/package.json b/backend/bridge/node_modules/body-parser/package.json new file mode 100644 index 00000000..486878a2 --- /dev/null +++ b/backend/bridge/node_modules/body-parser/package.json @@ -0,0 +1,52 @@ +{ + "name": "body-parser", + "description": "Node.js body parsing middleware", + "version": "2.2.2", + "contributors": [ + "Douglas Christopher Wilson ", + "Jonathan Ong (http://jongleberry.com)" + ], + "license": "MIT", + "repository": "expressjs/body-parser", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + }, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "devDependencies": { + "eslint": "^8.57.1", + "eslint-config-standard": "^14.1.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-markdown": "^3.0.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.6.0", + "eslint-plugin-standard": "^4.1.0", + "mocha": "^11.1.0", + "nyc": "^17.1.0", + "supertest": "^7.0.0" + }, + "files": [ + "lib/", + "LICENSE", + "index.js" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "lint": "eslint .", + "test": "mocha --reporter spec --check-leaks test/", + "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test" + } +} diff --git a/backend/bridge/node_modules/bytes/History.md b/backend/bridge/node_modules/bytes/History.md new file mode 100644 index 00000000..d60ce0e6 --- /dev/null +++ b/backend/bridge/node_modules/bytes/History.md @@ -0,0 +1,97 @@ +3.1.2 / 2022-01-27 +================== + + * Fix return value for un-parsable strings + +3.1.1 / 2021-11-15 +================== + + * Fix "thousandsSeparator" incorrecting formatting fractional part + +3.1.0 / 2019-01-22 +================== + + * Add petabyte (`pb`) support + +3.0.0 / 2017-08-31 +================== + + * Change "kB" to "KB" in format output + * Remove support for Node.js 0.6 + * Remove support for ComponentJS + +2.5.0 / 2017-03-24 +================== + + * Add option "unit" + +2.4.0 / 2016-06-01 +================== + + * Add option "unitSeparator" + +2.3.0 / 2016-02-15 +================== + + * Drop partial bytes on all parsed units + * Fix non-finite numbers to `.format` to return `null` + * Fix parsing byte string that looks like hex + * perf: hoist regular expressions + +2.2.0 / 2015-11-13 +================== + + * add option "decimalPlaces" + * add option "fixedDecimals" + +2.1.0 / 2015-05-21 +================== + + * add `.format` export + * add `.parse` export + +2.0.2 / 2015-05-20 +================== + + * remove map recreation + * remove unnecessary object construction + +2.0.1 / 2015-05-07 +================== + + * fix browserify require + * remove node.extend dependency + +2.0.0 / 2015-04-12 +================== + + * add option "case" + * add option "thousandsSeparator" + * return "null" on invalid parse input + * support proper round-trip: bytes(bytes(num)) === num + * units no longer case sensitive when parsing + +1.0.0 / 2014-05-05 +================== + + * add negative support. fixes #6 + +0.3.0 / 2014-03-19 +================== + + * added terabyte support + +0.2.1 / 2013-04-01 +================== + + * add .component + +0.2.0 / 2012-10-28 +================== + + * bytes(200).should.eql('200b') + +0.1.0 / 2012-07-04 +================== + + * add bytes to string conversion [yields] diff --git a/backend/bridge/node_modules/bytes/LICENSE b/backend/bridge/node_modules/bytes/LICENSE new file mode 100644 index 00000000..63e95a96 --- /dev/null +++ b/backend/bridge/node_modules/bytes/LICENSE @@ -0,0 +1,23 @@ +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/bridge/node_modules/bytes/Readme.md b/backend/bridge/node_modules/bytes/Readme.md new file mode 100644 index 00000000..5790e23e --- /dev/null +++ b/backend/bridge/node_modules/bytes/Readme.md @@ -0,0 +1,152 @@ +# Bytes utility + +[![NPM Version][npm-image]][npm-url] +[![NPM Downloads][downloads-image]][downloads-url] +[![Build Status][ci-image]][ci-url] +[![Test Coverage][coveralls-image]][coveralls-url] + +Utility to parse a string bytes (ex: `1TB`) to bytes (`1099511627776`) and vice-versa. + +## Installation + +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). Installation is done using the +[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): + +```bash +$ npm install bytes +``` + +## Usage + +```js +var bytes = require('bytes'); +``` + +#### bytes(number|string value, [options]): number|string|null + +Default export function. Delegates to either `bytes.format` or `bytes.parse` based on the type of `value`. + +**Arguments** + +| Name | Type | Description | +|---------|----------|--------------------| +| value | `number`|`string` | Number value to format or string value to parse | +| options | `Object` | Conversion options for `format` | + +**Returns** + +| Name | Type | Description | +|---------|------------------|-------------------------------------------------| +| results | `string`|`number`|`null` | Return null upon error. Numeric value in bytes, or string value otherwise. | + +**Example** + +```js +bytes(1024); +// output: '1KB' + +bytes('1KB'); +// output: 1024 +``` + +#### bytes.format(number value, [options]): string|null + +Format the given value in bytes into a string. If the value is negative, it is kept as such. If it is a float, it is + rounded. + +**Arguments** + +| Name | Type | Description | +|---------|----------|--------------------| +| value | `number` | Value in bytes | +| options | `Object` | Conversion options | + +**Options** + +| Property | Type | Description | +|-------------------|--------|-----------------------------------------------------------------------------------------| +| decimalPlaces | `number`|`null` | Maximum number of decimal places to include in output. Default value to `2`. | +| fixedDecimals | `boolean`|`null` | Whether to always display the maximum number of decimal places. Default value to `false` | +| thousandsSeparator | `string`|`null` | Example of values: `' '`, `','` and `'.'`... Default value to `''`. | +| unit | `string`|`null` | The unit in which the result will be returned (B/KB/MB/GB/TB). Default value to `''` (which means auto detect). | +| unitSeparator | `string`|`null` | Separator to use between number and unit. Default value to `''`. | + +**Returns** + +| Name | Type | Description | +|---------|------------------|-------------------------------------------------| +| results | `string`|`null` | Return null upon error. String value otherwise. | + +**Example** + +```js +bytes.format(1024); +// output: '1KB' + +bytes.format(1000); +// output: '1000B' + +bytes.format(1000, {thousandsSeparator: ' '}); +// output: '1 000B' + +bytes.format(1024 * 1.7, {decimalPlaces: 0}); +// output: '2KB' + +bytes.format(1024, {unitSeparator: ' '}); +// output: '1 KB' +``` + +#### bytes.parse(string|number value): number|null + +Parse the string value into an integer in bytes. If no unit is given, or `value` +is a number, it is assumed the value is in bytes. + +Supported units and abbreviations are as follows and are case-insensitive: + + * `b` for bytes + * `kb` for kilobytes + * `mb` for megabytes + * `gb` for gigabytes + * `tb` for terabytes + * `pb` for petabytes + +The units are in powers of two, not ten. This means 1kb = 1024b according to this parser. + +**Arguments** + +| Name | Type | Description | +|---------------|--------|--------------------| +| value | `string`|`number` | String to parse, or number in bytes. | + +**Returns** + +| Name | Type | Description | +|---------|-------------|-------------------------| +| results | `number`|`null` | Return null upon error. Value in bytes otherwise. | + +**Example** + +```js +bytes.parse('1KB'); +// output: 1024 + +bytes.parse('1024'); +// output: 1024 + +bytes.parse(1024); +// output: 1024 +``` + +## License + +[MIT](LICENSE) + +[ci-image]: https://badgen.net/github/checks/visionmedia/bytes.js/master?label=ci +[ci-url]: https://github.com/visionmedia/bytes.js/actions?query=workflow%3Aci +[coveralls-image]: https://badgen.net/coveralls/c/github/visionmedia/bytes.js/master +[coveralls-url]: https://coveralls.io/r/visionmedia/bytes.js?branch=master +[downloads-image]: https://badgen.net/npm/dm/bytes +[downloads-url]: https://npmjs.org/package/bytes +[npm-image]: https://badgen.net/npm/v/bytes +[npm-url]: https://npmjs.org/package/bytes diff --git a/backend/bridge/node_modules/bytes/index.js b/backend/bridge/node_modules/bytes/index.js new file mode 100644 index 00000000..6f2d0f89 --- /dev/null +++ b/backend/bridge/node_modules/bytes/index.js @@ -0,0 +1,170 @@ +/*! + * bytes + * Copyright(c) 2012-2014 TJ Holowaychuk + * Copyright(c) 2015 Jed Watson + * MIT Licensed + */ + +'use strict'; + +/** + * Module exports. + * @public + */ + +module.exports = bytes; +module.exports.format = format; +module.exports.parse = parse; + +/** + * Module variables. + * @private + */ + +var formatThousandsRegExp = /\B(?=(\d{3})+(?!\d))/g; + +var formatDecimalsRegExp = /(?:\.0*|(\.[^0]+)0+)$/; + +var map = { + b: 1, + kb: 1 << 10, + mb: 1 << 20, + gb: 1 << 30, + tb: Math.pow(1024, 4), + pb: Math.pow(1024, 5), +}; + +var parseRegExp = /^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i; + +/** + * Convert the given value in bytes into a string or parse to string to an integer in bytes. + * + * @param {string|number} value + * @param {{ + * case: [string], + * decimalPlaces: [number] + * fixedDecimals: [boolean] + * thousandsSeparator: [string] + * unitSeparator: [string] + * }} [options] bytes options. + * + * @returns {string|number|null} + */ + +function bytes(value, options) { + if (typeof value === 'string') { + return parse(value); + } + + if (typeof value === 'number') { + return format(value, options); + } + + return null; +} + +/** + * Format the given value in bytes into a string. + * + * If the value is negative, it is kept as such. If it is a float, + * it is rounded. + * + * @param {number} value + * @param {object} [options] + * @param {number} [options.decimalPlaces=2] + * @param {number} [options.fixedDecimals=false] + * @param {string} [options.thousandsSeparator=] + * @param {string} [options.unit=] + * @param {string} [options.unitSeparator=] + * + * @returns {string|null} + * @public + */ + +function format(value, options) { + if (!Number.isFinite(value)) { + return null; + } + + var mag = Math.abs(value); + var thousandsSeparator = (options && options.thousandsSeparator) || ''; + var unitSeparator = (options && options.unitSeparator) || ''; + var decimalPlaces = (options && options.decimalPlaces !== undefined) ? options.decimalPlaces : 2; + var fixedDecimals = Boolean(options && options.fixedDecimals); + var unit = (options && options.unit) || ''; + + if (!unit || !map[unit.toLowerCase()]) { + if (mag >= map.pb) { + unit = 'PB'; + } else if (mag >= map.tb) { + unit = 'TB'; + } else if (mag >= map.gb) { + unit = 'GB'; + } else if (mag >= map.mb) { + unit = 'MB'; + } else if (mag >= map.kb) { + unit = 'KB'; + } else { + unit = 'B'; + } + } + + var val = value / map[unit.toLowerCase()]; + var str = val.toFixed(decimalPlaces); + + if (!fixedDecimals) { + str = str.replace(formatDecimalsRegExp, '$1'); + } + + if (thousandsSeparator) { + str = str.split('.').map(function (s, i) { + return i === 0 + ? s.replace(formatThousandsRegExp, thousandsSeparator) + : s + }).join('.'); + } + + return str + unitSeparator + unit; +} + +/** + * Parse the string value into an integer in bytes. + * + * If no unit is given, it is assumed the value is in bytes. + * + * @param {number|string} val + * + * @returns {number|null} + * @public + */ + +function parse(val) { + if (typeof val === 'number' && !isNaN(val)) { + return val; + } + + if (typeof val !== 'string') { + return null; + } + + // Test if the string passed is valid + var results = parseRegExp.exec(val); + var floatValue; + var unit = 'b'; + + if (!results) { + // Nothing could be extracted from the given string + floatValue = parseInt(val, 10); + unit = 'b' + } else { + // Retrieve the value and the unit + floatValue = parseFloat(results[1]); + unit = results[4].toLowerCase(); + } + + if (isNaN(floatValue)) { + return null; + } + + return Math.floor(map[unit] * floatValue); +} diff --git a/backend/bridge/node_modules/bytes/package.json b/backend/bridge/node_modules/bytes/package.json new file mode 100644 index 00000000..f2b6a8b0 --- /dev/null +++ b/backend/bridge/node_modules/bytes/package.json @@ -0,0 +1,42 @@ +{ + "name": "bytes", + "description": "Utility to parse a string bytes to bytes and vice-versa", + "version": "3.1.2", + "author": "TJ Holowaychuk (http://tjholowaychuk.com)", + "contributors": [ + "Jed Watson ", + "Théo FIDRY " + ], + "license": "MIT", + "keywords": [ + "byte", + "bytes", + "utility", + "parse", + "parser", + "convert", + "converter" + ], + "repository": "visionmedia/bytes.js", + "devDependencies": { + "eslint": "7.32.0", + "eslint-plugin-markdown": "2.2.1", + "mocha": "9.2.0", + "nyc": "15.1.0" + }, + "files": [ + "History.md", + "LICENSE", + "Readme.md", + "index.js" + ], + "engines": { + "node": ">= 0.8" + }, + "scripts": { + "lint": "eslint .", + "test": "mocha --check-leaks --reporter spec", + "test-ci": "nyc --reporter=lcov --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test" + } +} diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/.eslintrc b/backend/bridge/node_modules/call-bind-apply-helpers/.eslintrc new file mode 100644 index 00000000..201e859b --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/.eslintrc @@ -0,0 +1,17 @@ +{ + "root": true, + + "extends": "@ljharb", + + "rules": { + "func-name-matching": 0, + "id-length": 0, + "new-cap": [2, { + "capIsNewExceptions": [ + "GetIntrinsic", + ], + }], + "no-extra-parens": 0, + "no-magic-numbers": 0, + }, +} diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/.github/FUNDING.yml b/backend/bridge/node_modules/call-bind-apply-helpers/.github/FUNDING.yml new file mode 100644 index 00000000..0011e9d6 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [ljharb] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: npm/call-bind-apply-helpers +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/.nycrc b/backend/bridge/node_modules/call-bind-apply-helpers/.nycrc new file mode 100644 index 00000000..bdd626ce --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/.nycrc @@ -0,0 +1,9 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text-summary", "text", "html", "json"], + "exclude": [ + "coverage", + "test" + ] +} diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/CHANGELOG.md b/backend/bridge/node_modules/call-bind-apply-helpers/CHANGELOG.md new file mode 100644 index 00000000..24849428 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v1.0.2](https://github.com/ljharb/call-bind-apply-helpers/compare/v1.0.1...v1.0.2) - 2025-02-12 + +### Commits + +- [types] improve inferred types [`e6f9586`](https://github.com/ljharb/call-bind-apply-helpers/commit/e6f95860a3c72879cb861a858cdfb8138fbedec1) +- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `@types/tape`, `es-value-fixtures`, `for-each`, `has-strict-mode`, `object-inspect` [`e43d540`](https://github.com/ljharb/call-bind-apply-helpers/commit/e43d5409f97543bfbb11f345d47d8ce4e066d8c1) + +## [v1.0.1](https://github.com/ljharb/call-bind-apply-helpers/compare/v1.0.0...v1.0.1) - 2024-12-08 + +### Commits + +- [types] `reflectApply`: fix types [`4efc396`](https://github.com/ljharb/call-bind-apply-helpers/commit/4efc3965351a4f02cc55e836fa391d3d11ef2ef8) +- [Fix] `reflectApply`: oops, Reflect is not a function [`83cc739`](https://github.com/ljharb/call-bind-apply-helpers/commit/83cc7395de6b79b7730bdf092f1436f0b1263c75) +- [Dev Deps] update `@arethetypeswrong/cli` [`80bd5d3`](https://github.com/ljharb/call-bind-apply-helpers/commit/80bd5d3ae58b4f6b6995ce439dd5a1bcb178a940) + +## v1.0.0 - 2024-12-05 + +### Commits + +- Initial implementation, tests, readme [`7879629`](https://github.com/ljharb/call-bind-apply-helpers/commit/78796290f9b7430c9934d6f33d94ae9bc89fce04) +- Initial commit [`3f1dc16`](https://github.com/ljharb/call-bind-apply-helpers/commit/3f1dc164afc43285631b114a5f9dd9137b2b952f) +- npm init [`081df04`](https://github.com/ljharb/call-bind-apply-helpers/commit/081df048c312fcee400922026f6e97281200a603) +- Only apps should have lockfiles [`5b9ca0f`](https://github.com/ljharb/call-bind-apply-helpers/commit/5b9ca0fe8101ebfaf309c549caac4e0a017ed930) diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/LICENSE b/backend/bridge/node_modules/call-bind-apply-helpers/LICENSE new file mode 100644 index 00000000..f82f3896 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/README.md b/backend/bridge/node_modules/call-bind-apply-helpers/README.md new file mode 100644 index 00000000..8fc0dae1 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/README.md @@ -0,0 +1,62 @@ +# call-bind-apply-helpers [![Version Badge][npm-version-svg]][package-url] + +[![github actions][actions-image]][actions-url] +[![coverage][codecov-image]][codecov-url] +[![dependency status][deps-svg]][deps-url] +[![dev dependency status][dev-deps-svg]][dev-deps-url] +[![License][license-image]][license-url] +[![Downloads][downloads-image]][downloads-url] + +[![npm badge][npm-badge-png]][package-url] + +Helper functions around Function call/apply/bind, for use in `call-bind`. + +The only packages that should likely ever use this package directly are `call-bind` and `get-intrinsic`. +Please use `call-bind` unless you have a very good reason not to. + +## Getting started + +```sh +npm install --save call-bind-apply-helpers +``` + +## Usage/Examples + +```js +const assert = require('assert'); +const callBindBasic = require('call-bind-apply-helpers'); + +function f(a, b) { + assert.equal(this, 1); + assert.equal(a, 2); + assert.equal(b, 3); + assert.equal(arguments.length, 2); +} + +const fBound = callBindBasic([f, 1]); + +delete Function.prototype.call; +delete Function.prototype.bind; + +fBound(2, 3); +``` + +## Tests + +Clone the repo, `npm install`, and run `npm test` + +[package-url]: https://npmjs.org/package/call-bind-apply-helpers +[npm-version-svg]: https://versionbadg.es/ljharb/call-bind-apply-helpers.svg +[deps-svg]: https://david-dm.org/ljharb/call-bind-apply-helpers.svg +[deps-url]: https://david-dm.org/ljharb/call-bind-apply-helpers +[dev-deps-svg]: https://david-dm.org/ljharb/call-bind-apply-helpers/dev-status.svg +[dev-deps-url]: https://david-dm.org/ljharb/call-bind-apply-helpers#info=devDependencies +[npm-badge-png]: https://nodei.co/npm/call-bind-apply-helpers.png?downloads=true&stars=true +[license-image]: https://img.shields.io/npm/l/call-bind-apply-helpers.svg +[license-url]: LICENSE +[downloads-image]: https://img.shields.io/npm/dm/call-bind-apply-helpers.svg +[downloads-url]: https://npm-stat.com/charts.html?package=call-bind-apply-helpers +[codecov-image]: https://codecov.io/gh/ljharb/call-bind-apply-helpers/branch/main/graphs/badge.svg +[codecov-url]: https://app.codecov.io/gh/ljharb/call-bind-apply-helpers/ +[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/call-bind-apply-helpers +[actions-url]: https://github.com/ljharb/call-bind-apply-helpers/actions diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/actualApply.d.ts b/backend/bridge/node_modules/call-bind-apply-helpers/actualApply.d.ts new file mode 100644 index 00000000..b87286a2 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/actualApply.d.ts @@ -0,0 +1 @@ +export = Reflect.apply; \ No newline at end of file diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/actualApply.js b/backend/bridge/node_modules/call-bind-apply-helpers/actualApply.js new file mode 100644 index 00000000..ffa51355 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/actualApply.js @@ -0,0 +1,10 @@ +'use strict'; + +var bind = require('function-bind'); + +var $apply = require('./functionApply'); +var $call = require('./functionCall'); +var $reflectApply = require('./reflectApply'); + +/** @type {import('./actualApply')} */ +module.exports = $reflectApply || bind.call($call, $apply); diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/applyBind.d.ts b/backend/bridge/node_modules/call-bind-apply-helpers/applyBind.d.ts new file mode 100644 index 00000000..d176c1ab --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/applyBind.d.ts @@ -0,0 +1,19 @@ +import actualApply from './actualApply'; + +type TupleSplitHead = T['length'] extends N + ? T + : T extends [...infer R, any] + ? TupleSplitHead + : never + +type TupleSplitTail = O['length'] extends N + ? T + : T extends [infer F, ...infer R] + ? TupleSplitTail<[...R], N, [...O, F]> + : never + +type TupleSplit = [TupleSplitHead, TupleSplitTail] + +declare function applyBind(...args: TupleSplit, 2>[1]): ReturnType; + +export = applyBind; \ No newline at end of file diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/applyBind.js b/backend/bridge/node_modules/call-bind-apply-helpers/applyBind.js new file mode 100644 index 00000000..d2b77231 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/applyBind.js @@ -0,0 +1,10 @@ +'use strict'; + +var bind = require('function-bind'); +var $apply = require('./functionApply'); +var actualApply = require('./actualApply'); + +/** @type {import('./applyBind')} */ +module.exports = function applyBind() { + return actualApply(bind, $apply, arguments); +}; diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/functionApply.d.ts b/backend/bridge/node_modules/call-bind-apply-helpers/functionApply.d.ts new file mode 100644 index 00000000..1f6e11b3 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/functionApply.d.ts @@ -0,0 +1 @@ +export = Function.prototype.apply; \ No newline at end of file diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/functionApply.js b/backend/bridge/node_modules/call-bind-apply-helpers/functionApply.js new file mode 100644 index 00000000..c71df9c2 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/functionApply.js @@ -0,0 +1,4 @@ +'use strict'; + +/** @type {import('./functionApply')} */ +module.exports = Function.prototype.apply; diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/functionCall.d.ts b/backend/bridge/node_modules/call-bind-apply-helpers/functionCall.d.ts new file mode 100644 index 00000000..15e93df3 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/functionCall.d.ts @@ -0,0 +1 @@ +export = Function.prototype.call; \ No newline at end of file diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/functionCall.js b/backend/bridge/node_modules/call-bind-apply-helpers/functionCall.js new file mode 100644 index 00000000..7a8d8735 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/functionCall.js @@ -0,0 +1,4 @@ +'use strict'; + +/** @type {import('./functionCall')} */ +module.exports = Function.prototype.call; diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/index.d.ts b/backend/bridge/node_modules/call-bind-apply-helpers/index.d.ts new file mode 100644 index 00000000..541516bd --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/index.d.ts @@ -0,0 +1,64 @@ +type RemoveFromTuple< + Tuple extends readonly unknown[], + RemoveCount extends number, + Index extends 1[] = [] +> = Index["length"] extends RemoveCount + ? Tuple + : Tuple extends [infer First, ...infer Rest] + ? RemoveFromTuple + : Tuple; + +type ConcatTuples< + Prefix extends readonly unknown[], + Suffix extends readonly unknown[] +> = [...Prefix, ...Suffix]; + +type ExtractFunctionParams = T extends (this: infer TThis, ...args: infer P extends readonly unknown[]) => infer R + ? { thisArg: TThis; params: P; returnType: R } + : never; + +type BindFunction< + T extends (this: any, ...args: any[]) => any, + TThis, + TBoundArgs extends readonly unknown[], + ReceiverBound extends boolean +> = ExtractFunctionParams extends { + thisArg: infer OrigThis; + params: infer P extends readonly unknown[]; + returnType: infer R; +} + ? ReceiverBound extends true + ? (...args: RemoveFromTuple>) => R extends [OrigThis, ...infer Rest] + ? [TThis, ...Rest] // Replace `this` with `thisArg` + : R + : >>( + thisArg: U, + ...args: RemainingArgs + ) => R extends [OrigThis, ...infer Rest] + ? [U, ...ConcatTuples] // Preserve bound args in return type + : R + : never; + +declare function callBind< + const T extends (this: any, ...args: any[]) => any, + Extracted extends ExtractFunctionParams, + const TBoundArgs extends Partial & readonly unknown[], + const TThis extends Extracted["thisArg"] +>( + args: [fn: T, thisArg: TThis, ...boundArgs: TBoundArgs] +): BindFunction; + +declare function callBind< + const T extends (this: any, ...args: any[]) => any, + Extracted extends ExtractFunctionParams, + const TBoundArgs extends Partial & readonly unknown[] +>( + args: [fn: T, ...boundArgs: TBoundArgs] +): BindFunction; + +declare function callBind( + args: [fn: Exclude, ...rest: TArgs] +): never; + +// export as namespace callBind; +export = callBind; diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/index.js b/backend/bridge/node_modules/call-bind-apply-helpers/index.js new file mode 100644 index 00000000..2f6dab4c --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/index.js @@ -0,0 +1,15 @@ +'use strict'; + +var bind = require('function-bind'); +var $TypeError = require('es-errors/type'); + +var $call = require('./functionCall'); +var $actualApply = require('./actualApply'); + +/** @type {(args: [Function, thisArg?: unknown, ...args: unknown[]]) => Function} TODO FIXME, find a way to use import('.') */ +module.exports = function callBindBasic(args) { + if (args.length < 1 || typeof args[0] !== 'function') { + throw new $TypeError('a function is required'); + } + return $actualApply(bind, $call, args); +}; diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/package.json b/backend/bridge/node_modules/call-bind-apply-helpers/package.json new file mode 100644 index 00000000..923b8be2 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/package.json @@ -0,0 +1,85 @@ +{ + "name": "call-bind-apply-helpers", + "version": "1.0.2", + "description": "Helper functions around Function call/apply/bind, for use in `call-bind`", + "main": "index.js", + "exports": { + ".": "./index.js", + "./actualApply": "./actualApply.js", + "./applyBind": "./applyBind.js", + "./functionApply": "./functionApply.js", + "./functionCall": "./functionCall.js", + "./reflectApply": "./reflectApply.js", + "./package.json": "./package.json" + }, + "scripts": { + "prepack": "npmignore --auto --commentLines=auto", + "prepublish": "not-in-publish || npm run prepublishOnly", + "prepublishOnly": "safe-publish-latest", + "prelint": "evalmd README.md", + "lint": "eslint --ext=.js,.mjs .", + "postlint": "tsc -p . && attw -P", + "pretest": "npm run lint", + "tests-only": "nyc tape 'test/**/*.js'", + "test": "npm run tests-only", + "posttest": "npx npm@'>=10.2' audit --production", + "version": "auto-changelog && git add CHANGELOG.md", + "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ljharb/call-bind-apply-helpers.git" + }, + "author": "Jordan Harband ", + "license": "MIT", + "bugs": { + "url": "https://github.com/ljharb/call-bind-apply-helpers/issues" + }, + "homepage": "https://github.com/ljharb/call-bind-apply-helpers#readme", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.17.3", + "@ljharb/eslint-config": "^21.1.1", + "@ljharb/tsconfig": "^0.2.3", + "@types/for-each": "^0.3.3", + "@types/function-bind": "^1.1.10", + "@types/object-inspect": "^1.13.0", + "@types/tape": "^5.8.1", + "auto-changelog": "^2.5.0", + "encoding": "^0.1.13", + "es-value-fixtures": "^1.7.1", + "eslint": "=8.8.0", + "evalmd": "^0.0.19", + "for-each": "^0.3.5", + "has-strict-mode": "^1.1.0", + "in-publish": "^2.0.1", + "npmignore": "^0.3.1", + "nyc": "^10.3.2", + "object-inspect": "^1.13.4", + "safe-publish-latest": "^2.0.0", + "tape": "^5.9.0", + "typescript": "next" + }, + "testling": { + "files": "test/index.js" + }, + "auto-changelog": { + "output": "CHANGELOG.md", + "template": "keepachangelog", + "unreleased": false, + "commitLimit": false, + "backfillLimit": false, + "hideCredit": true + }, + "publishConfig": { + "ignore": [ + ".github/workflows" + ] + }, + "engines": { + "node": ">= 0.4" + } +} diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/reflectApply.d.ts b/backend/bridge/node_modules/call-bind-apply-helpers/reflectApply.d.ts new file mode 100644 index 00000000..6b2ae764 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/reflectApply.d.ts @@ -0,0 +1,3 @@ +declare const reflectApply: false | typeof Reflect.apply; + +export = reflectApply; diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/reflectApply.js b/backend/bridge/node_modules/call-bind-apply-helpers/reflectApply.js new file mode 100644 index 00000000..3d03caa6 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/reflectApply.js @@ -0,0 +1,4 @@ +'use strict'; + +/** @type {import('./reflectApply')} */ +module.exports = typeof Reflect !== 'undefined' && Reflect && Reflect.apply; diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/test/index.js b/backend/bridge/node_modules/call-bind-apply-helpers/test/index.js new file mode 100644 index 00000000..1cdc89ed --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/test/index.js @@ -0,0 +1,63 @@ +'use strict'; + +var callBind = require('../'); +var hasStrictMode = require('has-strict-mode')(); +var forEach = require('for-each'); +var inspect = require('object-inspect'); +var v = require('es-value-fixtures'); + +var test = require('tape'); + +test('callBindBasic', function (t) { + forEach(v.nonFunctions, function (nonFunction) { + t['throws']( + // @ts-expect-error + function () { callBind([nonFunction]); }, + TypeError, + inspect(nonFunction) + ' is not a function' + ); + }); + + var sentinel = { sentinel: true }; + /** @type {(this: T, a: A, b: B) => [T | undefined, A, B]} */ + var func = function (a, b) { + // eslint-disable-next-line no-invalid-this + return [!hasStrictMode && this === global ? undefined : this, a, b]; + }; + t.equal(func.length, 2, 'original function length is 2'); + + /** type {(thisArg: unknown, a: number, b: number) => [unknown, number, number]} */ + var bound = callBind([func]); + /** type {((a: number, b: number) => [typeof sentinel, typeof a, typeof b])} */ + var boundR = callBind([func, sentinel]); + /** type {((b: number) => [typeof sentinel, number, typeof b])} */ + var boundArg = callBind([func, sentinel, /** @type {const} */ (1)]); + + // @ts-expect-error + t.deepEqual(bound(), [undefined, undefined, undefined], 'bound func with no args'); + + // @ts-expect-error + t.deepEqual(func(), [undefined, undefined, undefined], 'unbound func with too few args'); + // @ts-expect-error + t.deepEqual(bound(1, 2), [hasStrictMode ? 1 : Object(1), 2, undefined], 'bound func too few args'); + // @ts-expect-error + t.deepEqual(boundR(), [sentinel, undefined, undefined], 'bound func with receiver, with too few args'); + // @ts-expect-error + t.deepEqual(boundArg(), [sentinel, 1, undefined], 'bound func with receiver and arg, with too few args'); + + t.deepEqual(func(1, 2), [undefined, 1, 2], 'unbound func with right args'); + t.deepEqual(bound(1, 2, 3), [hasStrictMode ? 1 : Object(1), 2, 3], 'bound func with right args'); + t.deepEqual(boundR(1, 2), [sentinel, 1, 2], 'bound func with receiver, with right args'); + t.deepEqual(boundArg(2), [sentinel, 1, 2], 'bound func with receiver and arg, with right arg'); + + // @ts-expect-error + t.deepEqual(func(1, 2, 3), [undefined, 1, 2], 'unbound func with too many args'); + // @ts-expect-error + t.deepEqual(bound(1, 2, 3, 4), [hasStrictMode ? 1 : Object(1), 2, 3], 'bound func with too many args'); + // @ts-expect-error + t.deepEqual(boundR(1, 2, 3), [sentinel, 1, 2], 'bound func with receiver, with too many args'); + // @ts-expect-error + t.deepEqual(boundArg(2, 3), [sentinel, 1, 2], 'bound func with receiver and arg, with too many args'); + + t.end(); +}); diff --git a/backend/bridge/node_modules/call-bind-apply-helpers/tsconfig.json b/backend/bridge/node_modules/call-bind-apply-helpers/tsconfig.json new file mode 100644 index 00000000..aef99930 --- /dev/null +++ b/backend/bridge/node_modules/call-bind-apply-helpers/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@ljharb/tsconfig", + "compilerOptions": { + "target": "es2021", + }, + "exclude": [ + "coverage", + ], +} \ No newline at end of file diff --git a/backend/bridge/node_modules/call-bound/.eslintrc b/backend/bridge/node_modules/call-bound/.eslintrc new file mode 100644 index 00000000..2612ed8f --- /dev/null +++ b/backend/bridge/node_modules/call-bound/.eslintrc @@ -0,0 +1,13 @@ +{ + "root": true, + + "extends": "@ljharb", + + "rules": { + "new-cap": [2, { + "capIsNewExceptions": [ + "GetIntrinsic", + ], + }], + }, +} diff --git a/backend/bridge/node_modules/call-bound/.github/FUNDING.yml b/backend/bridge/node_modules/call-bound/.github/FUNDING.yml new file mode 100644 index 00000000..2a2a1357 --- /dev/null +++ b/backend/bridge/node_modules/call-bound/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [ljharb] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: npm/call-bound +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/backend/bridge/node_modules/call-bound/.nycrc b/backend/bridge/node_modules/call-bound/.nycrc new file mode 100644 index 00000000..bdd626ce --- /dev/null +++ b/backend/bridge/node_modules/call-bound/.nycrc @@ -0,0 +1,9 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text-summary", "text", "html", "json"], + "exclude": [ + "coverage", + "test" + ] +} diff --git a/backend/bridge/node_modules/call-bound/CHANGELOG.md b/backend/bridge/node_modules/call-bound/CHANGELOG.md new file mode 100644 index 00000000..8bde4e9a --- /dev/null +++ b/backend/bridge/node_modules/call-bound/CHANGELOG.md @@ -0,0 +1,42 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v1.0.4](https://github.com/ljharb/call-bound/compare/v1.0.3...v1.0.4) - 2025-03-03 + +### Commits + +- [types] improve types [`e648922`](https://github.com/ljharb/call-bound/commit/e6489222a9e54f350fbf952ceabe51fd8b6027ff) +- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `@types/tape`, `es-value-fixtures`, `for-each`, `has-strict-mode`, `object-inspect` [`a42a5eb`](https://github.com/ljharb/call-bound/commit/a42a5ebe6c1b54fcdc7997c7dc64fdca9e936719) +- [Deps] update `call-bind-apply-helpers`, `get-intrinsic` [`f529eac`](https://github.com/ljharb/call-bound/commit/f529eac132404c17156bbc23ab2297a25d0f20b8) + +## [v1.0.3](https://github.com/ljharb/call-bound/compare/v1.0.2...v1.0.3) - 2024-12-15 + +### Commits + +- [Refactor] use `call-bind-apply-helpers` instead of `call-bind` [`5e0b134`](https://github.com/ljharb/call-bound/commit/5e0b13496df14fb7d05dae9412f088da8d3f75be) +- [Deps] update `get-intrinsic` [`41fc967`](https://github.com/ljharb/call-bound/commit/41fc96732a22c7b7e8f381f93ccc54bb6293be2e) +- [readme] fix example [`79a0137`](https://github.com/ljharb/call-bound/commit/79a0137723f7c6d09c9c05452bbf8d5efb5d6e49) +- [meta] add `sideEffects` flag [`08b07be`](https://github.com/ljharb/call-bound/commit/08b07be7f1c03f67dc6f3cdaf0906259771859f7) + +## [v1.0.2](https://github.com/ljharb/call-bound/compare/v1.0.1...v1.0.2) - 2024-12-10 + +### Commits + +- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `gopd` [`e6a5ffe`](https://github.com/ljharb/call-bound/commit/e6a5ffe849368fe4f74dfd6cdeca1b9baa39e8d5) +- [Deps] update `call-bind`, `get-intrinsic` [`2aeb5b5`](https://github.com/ljharb/call-bound/commit/2aeb5b521dc2b2683d1345c753ea1161de2d1c14) +- [types] improve return type [`1a0c9fe`](https://github.com/ljharb/call-bound/commit/1a0c9fe3114471e7ca1f57d104e2efe713bb4871) + +## v1.0.1 - 2024-12-05 + +### Commits + +- Initial implementation, tests, readme, types [`6d94121`](https://github.com/ljharb/call-bound/commit/6d94121a9243602e506334069f7a03189fe3363d) +- Initial commit [`0eae867`](https://github.com/ljharb/call-bound/commit/0eae867334ea025c33e6e91cdecfc9df96680cf9) +- npm init [`71b2479`](https://github.com/ljharb/call-bound/commit/71b2479c6723e0b7d91a6b663613067e98b7b275) +- Only apps should have lockfiles [`c3754a9`](https://github.com/ljharb/call-bound/commit/c3754a949b7f9132b47e2d18c1729889736741eb) +- [actions] skip `npm ls` in node < 10 [`74275a5`](https://github.com/ljharb/call-bound/commit/74275a5186b8caf6309b6b97472bdcb0df4683a8) +- [Dev Deps] add missing peer dep [`1354de8`](https://github.com/ljharb/call-bound/commit/1354de8679413e4ae9c523d85f76fa7a5e032d97) diff --git a/backend/bridge/node_modules/call-bound/LICENSE b/backend/bridge/node_modules/call-bound/LICENSE new file mode 100644 index 00000000..f82f3896 --- /dev/null +++ b/backend/bridge/node_modules/call-bound/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/bridge/node_modules/call-bound/README.md b/backend/bridge/node_modules/call-bound/README.md new file mode 100644 index 00000000..a44e43e5 --- /dev/null +++ b/backend/bridge/node_modules/call-bound/README.md @@ -0,0 +1,53 @@ +# call-bound [![Version Badge][npm-version-svg]][package-url] + +[![github actions][actions-image]][actions-url] +[![coverage][codecov-image]][codecov-url] +[![dependency status][deps-svg]][deps-url] +[![dev dependency status][dev-deps-svg]][dev-deps-url] +[![License][license-image]][license-url] +[![Downloads][downloads-image]][downloads-url] + +[![npm badge][npm-badge-png]][package-url] + +Robust call-bound JavaScript intrinsics, using `call-bind` and `get-intrinsic`. + +## Getting started + +```sh +npm install --save call-bound +``` + +## Usage/Examples + +```js +const assert = require('assert'); +const callBound = require('call-bound'); + +const slice = callBound('Array.prototype.slice'); + +delete Function.prototype.call; +delete Function.prototype.bind; +delete Array.prototype.slice; + +assert.deepEqual(slice([1, 2, 3, 4], 1, -1), [2, 3]); +``` + +## Tests + +Clone the repo, `npm install`, and run `npm test` + +[package-url]: https://npmjs.org/package/call-bound +[npm-version-svg]: https://versionbadg.es/ljharb/call-bound.svg +[deps-svg]: https://david-dm.org/ljharb/call-bound.svg +[deps-url]: https://david-dm.org/ljharb/call-bound +[dev-deps-svg]: https://david-dm.org/ljharb/call-bound/dev-status.svg +[dev-deps-url]: https://david-dm.org/ljharb/call-bound#info=devDependencies +[npm-badge-png]: https://nodei.co/npm/call-bound.png?downloads=true&stars=true +[license-image]: https://img.shields.io/npm/l/call-bound.svg +[license-url]: LICENSE +[downloads-image]: https://img.shields.io/npm/dm/call-bound.svg +[downloads-url]: https://npm-stat.com/charts.html?package=call-bound +[codecov-image]: https://codecov.io/gh/ljharb/call-bound/branch/main/graphs/badge.svg +[codecov-url]: https://app.codecov.io/gh/ljharb/call-bound/ +[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/call-bound +[actions-url]: https://github.com/ljharb/call-bound/actions diff --git a/backend/bridge/node_modules/call-bound/index.d.ts b/backend/bridge/node_modules/call-bound/index.d.ts new file mode 100644 index 00000000..5562f00e --- /dev/null +++ b/backend/bridge/node_modules/call-bound/index.d.ts @@ -0,0 +1,94 @@ +type Intrinsic = typeof globalThis; + +type IntrinsicName = keyof Intrinsic | `%${keyof Intrinsic}%`; + +type IntrinsicPath = IntrinsicName | `${StripPercents}.${string}` | `%${StripPercents}.${string}%`; + +type AllowMissing = boolean; + +type StripPercents = T extends `%${infer U}%` ? U : T; + +type BindMethodPrecise = + F extends (this: infer This, ...args: infer Args) => infer R + ? (obj: This, ...args: Args) => R + : F extends { + (this: infer This1, ...args: infer Args1): infer R1; + (this: infer This2, ...args: infer Args2): infer R2 + } + ? { + (obj: This1, ...args: Args1): R1; + (obj: This2, ...args: Args2): R2 + } + : never + +// Extract method type from a prototype +type GetPrototypeMethod = + (typeof globalThis)[T] extends { prototype: any } + ? M extends keyof (typeof globalThis)[T]['prototype'] + ? (typeof globalThis)[T]['prototype'][M] + : never + : never + +// Get static property/method +type GetStaticMember = + P extends keyof (typeof globalThis)[T] ? (typeof globalThis)[T][P] : never + +// Type that maps string path to actual bound function or value with better precision +type BoundIntrinsic = + S extends `${infer Obj}.prototype.${infer Method}` + ? Obj extends keyof typeof globalThis + ? BindMethodPrecise> + : unknown + : S extends `${infer Obj}.${infer Prop}` + ? Obj extends keyof typeof globalThis + ? GetStaticMember + : unknown + : unknown + +declare function arraySlice(array: readonly T[], start?: number, end?: number): T[]; +declare function arraySlice(array: ArrayLike, start?: number, end?: number): T[]; +declare function arraySlice(array: IArguments, start?: number, end?: number): T[]; + +// Special cases for methods that need explicit typing +interface SpecialCases { + '%Object.prototype.isPrototypeOf%': (thisArg: {}, obj: unknown) => boolean; + '%String.prototype.replace%': { + (str: string, searchValue: string | RegExp, replaceValue: string): string; + (str: string, searchValue: string | RegExp, replacer: (substring: string, ...args: any[]) => string): string + }; + '%Object.prototype.toString%': (obj: {}) => string; + '%Object.prototype.hasOwnProperty%': (obj: {}, v: PropertyKey) => boolean; + '%Array.prototype.slice%': typeof arraySlice; + '%Array.prototype.map%': (array: readonly T[], callbackfn: (value: T, index: number, array: readonly T[]) => U, thisArg?: any) => U[]; + '%Array.prototype.filter%': (array: readonly T[], predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any) => T[]; + '%Array.prototype.indexOf%': (array: readonly T[], searchElement: T, fromIndex?: number) => number; + '%Function.prototype.apply%': (fn: (...args: A) => R, thisArg: any, args: A) => R; + '%Function.prototype.call%': (fn: (...args: A) => R, thisArg: any, ...args: A) => R; + '%Function.prototype.bind%': (fn: (...args: A) => R, thisArg: any, ...args: A) => (...remainingArgs: A) => R; + '%Promise.prototype.then%': { + (promise: Promise, onfulfilled: (value: T) => R | PromiseLike): Promise; + (promise: Promise, onfulfilled: ((value: T) => R | PromiseLike) | undefined | null, onrejected: (reason: any) => R | PromiseLike): Promise; + }; + '%RegExp.prototype.test%': (regexp: RegExp, str: string) => boolean; + '%RegExp.prototype.exec%': (regexp: RegExp, str: string) => RegExpExecArray | null; + '%Error.prototype.toString%': (error: Error) => string; + '%TypeError.prototype.toString%': (error: TypeError) => string; + '%String.prototype.split%': ( + obj: unknown, + splitter: string | RegExp | { + [Symbol.split](string: string, limit?: number): string[]; + }, + limit?: number | undefined + ) => string[]; +} + +/** + * Returns a bound function for a prototype method, or a value for a static property. + * + * @param name - The name of the intrinsic (e.g. 'Array.prototype.slice') + * @param {AllowMissing} [allowMissing] - Whether to allow missing intrinsics (default: false) + */ +declare function callBound, S extends IntrinsicPath>(name: K, allowMissing?: AllowMissing): SpecialCases[`%${StripPercents}%`]; +declare function callBound, S extends IntrinsicPath>(name: S, allowMissing?: AllowMissing): BoundIntrinsic; + +export = callBound; diff --git a/backend/bridge/node_modules/call-bound/index.js b/backend/bridge/node_modules/call-bound/index.js new file mode 100644 index 00000000..e9ade749 --- /dev/null +++ b/backend/bridge/node_modules/call-bound/index.js @@ -0,0 +1,19 @@ +'use strict'; + +var GetIntrinsic = require('get-intrinsic'); + +var callBindBasic = require('call-bind-apply-helpers'); + +/** @type {(thisArg: string, searchString: string, position?: number) => number} */ +var $indexOf = callBindBasic([GetIntrinsic('%String.prototype.indexOf%')]); + +/** @type {import('.')} */ +module.exports = function callBoundIntrinsic(name, allowMissing) { + /* eslint no-extra-parens: 0 */ + + var intrinsic = /** @type {(this: unknown, ...args: unknown[]) => unknown} */ (GetIntrinsic(name, !!allowMissing)); + if (typeof intrinsic === 'function' && $indexOf(name, '.prototype.') > -1) { + return callBindBasic(/** @type {const} */ ([intrinsic])); + } + return intrinsic; +}; diff --git a/backend/bridge/node_modules/call-bound/package.json b/backend/bridge/node_modules/call-bound/package.json new file mode 100644 index 00000000..d542db43 --- /dev/null +++ b/backend/bridge/node_modules/call-bound/package.json @@ -0,0 +1,99 @@ +{ + "name": "call-bound", + "version": "1.0.4", + "description": "Robust call-bound JavaScript intrinsics, using `call-bind` and `get-intrinsic`.", + "main": "index.js", + "exports": { + ".": "./index.js", + "./package.json": "./package.json" + }, + "sideEffects": false, + "scripts": { + "prepack": "npmignore --auto --commentLines=auto", + "prepublish": "not-in-publish || npm run prepublishOnly", + "prepublishOnly": "safe-publish-latest", + "prelint": "evalmd README.md", + "lint": "eslint --ext=.js,.mjs .", + "postlint": "tsc -p . && attw -P", + "pretest": "npm run lint", + "tests-only": "nyc tape 'test/**/*.js'", + "test": "npm run tests-only", + "posttest": "npx npm@'>=10.2' audit --production", + "version": "auto-changelog && git add CHANGELOG.md", + "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ljharb/call-bound.git" + }, + "keywords": [ + "javascript", + "ecmascript", + "es", + "js", + "callbind", + "callbound", + "call", + "bind", + "bound", + "call-bind", + "call-bound", + "function", + "es-abstract" + ], + "author": "Jordan Harband ", + "funding": { + "url": "https://github.com/sponsors/ljharb" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/ljharb/call-bound/issues" + }, + "homepage": "https://github.com/ljharb/call-bound#readme", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.17.4", + "@ljharb/eslint-config": "^21.1.1", + "@ljharb/tsconfig": "^0.3.0", + "@types/call-bind": "^1.0.5", + "@types/get-intrinsic": "^1.2.3", + "@types/tape": "^5.8.1", + "auto-changelog": "^2.5.0", + "encoding": "^0.1.13", + "es-value-fixtures": "^1.7.1", + "eslint": "=8.8.0", + "evalmd": "^0.0.19", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "has-strict-mode": "^1.1.0", + "in-publish": "^2.0.1", + "npmignore": "^0.3.1", + "nyc": "^10.3.2", + "object-inspect": "^1.13.4", + "safe-publish-latest": "^2.0.0", + "tape": "^5.9.0", + "typescript": "next" + }, + "testling": { + "files": "test/index.js" + }, + "auto-changelog": { + "output": "CHANGELOG.md", + "template": "keepachangelog", + "unreleased": false, + "commitLimit": false, + "backfillLimit": false, + "hideCredit": true + }, + "publishConfig": { + "ignore": [ + ".github/workflows" + ] + }, + "engines": { + "node": ">= 0.4" + } +} diff --git a/backend/bridge/node_modules/call-bound/test/index.js b/backend/bridge/node_modules/call-bound/test/index.js new file mode 100644 index 00000000..a2fc9f0f --- /dev/null +++ b/backend/bridge/node_modules/call-bound/test/index.js @@ -0,0 +1,61 @@ +'use strict'; + +var test = require('tape'); + +var callBound = require('../'); + +/** @template {true} T @template U @typedef {T extends U ? T : never} AssertType */ + +test('callBound', function (t) { + // static primitive + t.equal(callBound('Array.length'), Array.length, 'Array.length yields itself'); + t.equal(callBound('%Array.length%'), Array.length, '%Array.length% yields itself'); + + // static non-function object + t.equal(callBound('Array.prototype'), Array.prototype, 'Array.prototype yields itself'); + t.equal(callBound('%Array.prototype%'), Array.prototype, '%Array.prototype% yields itself'); + t.equal(callBound('Array.constructor'), Array.constructor, 'Array.constructor yields itself'); + t.equal(callBound('%Array.constructor%'), Array.constructor, '%Array.constructor% yields itself'); + + // static function + t.equal(callBound('Date.parse'), Date.parse, 'Date.parse yields itself'); + t.equal(callBound('%Date.parse%'), Date.parse, '%Date.parse% yields itself'); + + // prototype primitive + t.equal(callBound('Error.prototype.message'), Error.prototype.message, 'Error.prototype.message yields itself'); + t.equal(callBound('%Error.prototype.message%'), Error.prototype.message, '%Error.prototype.message% yields itself'); + + var x = callBound('Object.prototype.toString'); + var y = callBound('%Object.prototype.toString%'); + + // prototype function + t.notEqual(x, Object.prototype.toString, 'Object.prototype.toString does not yield itself'); + t.notEqual(y, Object.prototype.toString, '%Object.prototype.toString% does not yield itself'); + t.equal(x(true), Object.prototype.toString.call(true), 'call-bound Object.prototype.toString calls into the original'); + t.equal(y(true), Object.prototype.toString.call(true), 'call-bound %Object.prototype.toString% calls into the original'); + + t['throws']( + // @ts-expect-error + function () { callBound('does not exist'); }, + SyntaxError, + 'nonexistent intrinsic throws' + ); + t['throws']( + // @ts-expect-error + function () { callBound('does not exist', true); }, + SyntaxError, + 'allowMissing arg still throws for unknown intrinsic' + ); + + t.test('real but absent intrinsic', { skip: typeof WeakRef !== 'undefined' }, function (st) { + st['throws']( + function () { callBound('WeakRef'); }, + TypeError, + 'real but absent intrinsic throws' + ); + st.equal(callBound('WeakRef', true), undefined, 'allowMissing arg avoids exception'); + st.end(); + }); + + t.end(); +}); diff --git a/backend/bridge/node_modules/call-bound/tsconfig.json b/backend/bridge/node_modules/call-bound/tsconfig.json new file mode 100644 index 00000000..8976d98b --- /dev/null +++ b/backend/bridge/node_modules/call-bound/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@ljharb/tsconfig", + "compilerOptions": { + "target": "ESNext", + "lib": ["es2024"], + }, + "exclude": [ + "coverage", + ], +} diff --git a/backend/bridge/node_modules/content-disposition/HISTORY.md b/backend/bridge/node_modules/content-disposition/HISTORY.md new file mode 100644 index 00000000..1a3b308b --- /dev/null +++ b/backend/bridge/node_modules/content-disposition/HISTORY.md @@ -0,0 +1,72 @@ +1.0.1 / 2025-11-18 +================= + + * Updated `engines` field to Node@18 or higher (fixed reference, see 1.0.0) + * Remove dependency `safe-buffer` + +1.0.0 / 2024-08-31 +================== + + * drop node <18 + * allow utf8 as alias for utf-8 + +0.5.4 / 2021-12-10 +================== + + * deps: safe-buffer@5.2.1 + +0.5.3 / 2018-12-17 +================== + + * Use `safe-buffer` for improved Buffer API + +0.5.2 / 2016-12-08 +================== + + * Fix `parse` to accept any linear whitespace character + +0.5.1 / 2016-01-17 +================== + + * perf: enable strict mode + +0.5.0 / 2014-10-11 +================== + + * Add `parse` function + +0.4.0 / 2014-09-21 +================== + + * Expand non-Unicode `filename` to the full ISO-8859-1 charset + +0.3.0 / 2014-09-20 +================== + + * Add `fallback` option + * Add `type` option + +0.2.0 / 2014-09-19 +================== + + * Reduce ambiguity of file names with hex escape in buggy browsers + +0.1.2 / 2014-09-19 +================== + + * Fix periodic invalid Unicode filename header + +0.1.1 / 2014-09-19 +================== + + * Fix invalid characters appearing in `filename*` parameter + +0.1.0 / 2014-09-18 +================== + + * Make the `filename` argument optional + +0.0.0 / 2014-09-18 +================== + + * Initial release diff --git a/backend/bridge/node_modules/content-disposition/LICENSE b/backend/bridge/node_modules/content-disposition/LICENSE new file mode 100644 index 00000000..84441fbb --- /dev/null +++ b/backend/bridge/node_modules/content-disposition/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/bridge/node_modules/content-disposition/README.md b/backend/bridge/node_modules/content-disposition/README.md new file mode 100644 index 00000000..fbedc2f8 --- /dev/null +++ b/backend/bridge/node_modules/content-disposition/README.md @@ -0,0 +1,142 @@ +# content-disposition + +[![NPM Version][npm-image]][npm-url] +[![NPM Downloads][downloads-image]][downloads-url] +[![Node.js Version][node-version-image]][node-version-url] +[![Build Status][github-actions-ci-image]][github-actions-ci-url] +[![Test Coverage][coveralls-image]][coveralls-url] + +Create and parse HTTP `Content-Disposition` header + +## Installation + +```sh +$ npm install content-disposition +``` + +## API + +```js +const contentDisposition = require('content-disposition') +``` + +### contentDisposition(filename, options) + +Create an attachment `Content-Disposition` header value using the given file name, +if supplied. The `filename` is optional and if no file name is desired, but you +want to specify `options`, set `filename` to `undefined`. + +```js +res.setHeader('Content-Disposition', contentDisposition('∫ maths.pdf')) +``` + +**note** HTTP headers are of the ISO-8859-1 character set. If you are writing this +header through a means different from `setHeader` in Node.js, you'll want to specify +the `'binary'` encoding in Node.js. + +#### Options + +`contentDisposition` accepts these properties in the options object. + +##### fallback + +If the `filename` option is outside ISO-8859-1, then the file name is actually +stored in a supplemental field for clients that support Unicode file names and +a ISO-8859-1 version of the file name is automatically generated. + +This specifies the ISO-8859-1 file name to override the automatic generation or +disables the generation all together, defaults to `true`. + + - A string will specify the ISO-8859-1 file name to use in place of automatic + generation. + - `false` will disable including a ISO-8859-1 file name and only include the + Unicode version (unless the file name is already ISO-8859-1). + - `true` will enable automatic generation if the file name is outside ISO-8859-1. + +If the `filename` option is ISO-8859-1 and this option is specified and has a +different value, then the `filename` option is encoded in the extended field +and this set as the fallback field, even though they are both ISO-8859-1. + +##### type + +Specifies the disposition type, defaults to `"attachment"`. This can also be +`"inline"`, or any other value (all values except inline are treated like +`attachment`, but can convey additional information if both parties agree to +it). The type is normalized to lower-case. + +### contentDisposition.parse(string) + +```js +const disposition = contentDisposition.parse('attachment; filename="EURO rates.txt"; filename*=UTF-8\'\'%e2%82%ac%20rates.txt') +``` + +Parse a `Content-Disposition` header string. This automatically handles extended +("Unicode") parameters by decoding them and providing them under the standard +parameter name. This will return an object with the following properties (examples +are shown for the string `'attachment; filename="EURO rates.txt"; filename*=UTF-8\'\'%e2%82%ac%20rates.txt'`): + + - `type`: The disposition type (always lower case). Example: `'attachment'` + + - `parameters`: An object of the parameters in the disposition (name of parameter + always lower case and extended versions replace non-extended versions). Example: + `{filename: "€ rates.txt"}` + +## Examples + +### Send a file for download + +```js +const contentDisposition = require('content-disposition') +const destroy = require('destroy') +const fs = require('fs') +const http = require('http') +const onFinished = require('on-finished') + +const filePath = '/path/to/public/plans.pdf' + +http.createServer(function onRequest (req, res) { + // set headers + res.setHeader('Content-Type', 'application/pdf') + res.setHeader('Content-Disposition', contentDisposition(filePath)) + + // send file + const stream = fs.createReadStream(filePath) + stream.pipe(res) + onFinished(res, function () { + destroy(stream) + }) +}) +``` + +## Testing + +```sh +$ npm test +``` + +## References + +- [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1][rfc-2616] +- [RFC 5987: Character Set and Language Encoding for Hypertext Transfer Protocol (HTTP) Header Field Parameters][rfc-5987] +- [RFC 6266: Use of the Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)][rfc-6266] +- [Test Cases for HTTP Content-Disposition header field (RFC 6266) and the Encodings defined in RFCs 2047, 2231 and 5987][tc-2231] + +[rfc-2616]: https://tools.ietf.org/html/rfc2616 +[rfc-5987]: https://tools.ietf.org/html/rfc5987 +[rfc-6266]: https://tools.ietf.org/html/rfc6266 +[tc-2231]: http://greenbytes.de/tech/tc2231/ + +## License + +[MIT](LICENSE) + +[npm-image]: https://img.shields.io/npm/v/content-disposition +[npm-url]: https://npmjs.org/package/content-disposition +[node-version-image]: https://img.shields.io/node/v/content-disposition +[node-version-url]: https://nodejs.org/en/download +[coveralls-image]: https://img.shields.io/coverallsCoverage/github/jshttp/content-disposition +[coveralls-url]: https://coveralls.io/r/jshttp/content-disposition?branch=master +[downloads-image]: https://img.shields.io/npm/dm/content-disposition +[downloads-url]: https://npmjs.org/package/content-disposition +[github-actions-ci-image]: https://img.shields.io/github/actions/workflow/status/jshttp/content-disposition/ci.yml +[github-actions-ci-url]: https://github.com/jshttp/content-disposition/actions/workflows/ci.yml diff --git a/backend/bridge/node_modules/content-disposition/index.js b/backend/bridge/node_modules/content-disposition/index.js new file mode 100644 index 00000000..efcd9ca7 --- /dev/null +++ b/backend/bridge/node_modules/content-disposition/index.js @@ -0,0 +1,458 @@ +/*! + * content-disposition + * Copyright(c) 2014-2017 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module exports. + * @public + */ + +module.exports = contentDisposition +module.exports.parse = parse + +/** + * Module dependencies. + * @private + */ + +var basename = require('path').basename + +/** + * RegExp to match non attr-char, *after* encodeURIComponent (i.e. not including "%") + * @private + */ + +var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g // eslint-disable-line no-control-regex + +/** + * RegExp to match percent encoding escape. + * @private + */ + +var HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/ +var HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g + +/** + * RegExp to match non-latin1 characters. + * @private + */ + +var NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g + +/** + * RegExp to match quoted-pair in RFC 2616 + * + * quoted-pair = "\" CHAR + * CHAR = + * @private + */ + +var QESC_REGEXP = /\\([\u0000-\u007f])/g // eslint-disable-line no-control-regex + +/** + * RegExp to match chars that must be quoted-pair in RFC 2616 + * @private + */ + +var QUOTE_REGEXP = /([\\"])/g + +/** + * RegExp for various RFC 2616 grammar + * + * parameter = token "=" ( token | quoted-string ) + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + * qdtext = > + * quoted-pair = "\" CHAR + * CHAR = + * TEXT = + * LWS = [CRLF] 1*( SP | HT ) + * CRLF = CR LF + * CR = + * LF = + * SP = + * HT = + * CTL = + * OCTET = + * @private + */ + +var PARAM_REGEXP = /;[\x09\x20]*([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*=[\x09\x20]*("(?:[\x20!\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\x20-\x7e])*"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*/g // eslint-disable-line no-control-regex +var TEXT_REGEXP = /^[\x20-\x7e\x80-\xff]+$/ +var TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/ + +/** + * RegExp for various RFC 5987 grammar + * + * ext-value = charset "'" [ language ] "'" value-chars + * charset = "UTF-8" / "ISO-8859-1" / mime-charset + * mime-charset = 1*mime-charsetc + * mime-charsetc = ALPHA / DIGIT + * / "!" / "#" / "$" / "%" / "&" + * / "+" / "-" / "^" / "_" / "`" + * / "{" / "}" / "~" + * language = ( 2*3ALPHA [ extlang ] ) + * / 4ALPHA + * / 5*8ALPHA + * extlang = *3( "-" 3ALPHA ) + * value-chars = *( pct-encoded / attr-char ) + * pct-encoded = "%" HEXDIG HEXDIG + * attr-char = ALPHA / DIGIT + * / "!" / "#" / "$" / "&" / "+" / "-" / "." + * / "^" / "_" / "`" / "|" / "~" + * @private + */ + +var EXT_VALUE_REGEXP = /^([A-Za-z0-9!#$%&+\-^_`{}~]+)'(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8}|)'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)$/ + +/** + * RegExp for various RFC 6266 grammar + * + * disposition-type = "inline" | "attachment" | disp-ext-type + * disp-ext-type = token + * disposition-parm = filename-parm | disp-ext-parm + * filename-parm = "filename" "=" value + * | "filename*" "=" ext-value + * disp-ext-parm = token "=" value + * | ext-token "=" ext-value + * ext-token = + * @private + */ + +var DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*(?:$|;)/ // eslint-disable-line no-control-regex + +/** + * Create an attachment Content-Disposition header. + * + * @param {string} [filename] + * @param {object} [options] + * @param {string} [options.type=attachment] + * @param {string|boolean} [options.fallback=true] + * @return {string} + * @public + */ + +function contentDisposition (filename, options) { + var opts = options || {} + + // get type + var type = opts.type || 'attachment' + + // get parameters + var params = createparams(filename, opts.fallback) + + // format into string + return format(new ContentDisposition(type, params)) +} + +/** + * Create parameters object from filename and fallback. + * + * @param {string} [filename] + * @param {string|boolean} [fallback=true] + * @return {object} + * @private + */ + +function createparams (filename, fallback) { + if (filename === undefined) { + return + } + + var params = {} + + if (typeof filename !== 'string') { + throw new TypeError('filename must be a string') + } + + // fallback defaults to true + if (fallback === undefined) { + fallback = true + } + + if (typeof fallback !== 'string' && typeof fallback !== 'boolean') { + throw new TypeError('fallback must be a string or boolean') + } + + if (typeof fallback === 'string' && NON_LATIN1_REGEXP.test(fallback)) { + throw new TypeError('fallback must be ISO-8859-1 string') + } + + // restrict to file base name + var name = basename(filename) + + // determine if name is suitable for quoted string + var isQuotedString = TEXT_REGEXP.test(name) + + // generate fallback name + var fallbackName = typeof fallback !== 'string' + ? fallback && getlatin1(name) + : basename(fallback) + var hasFallback = typeof fallbackName === 'string' && fallbackName !== name + + // set extended filename parameter + if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) { + params['filename*'] = name + } + + // set filename parameter + if (isQuotedString || hasFallback) { + params.filename = hasFallback + ? fallbackName + : name + } + + return params +} + +/** + * Format object to Content-Disposition header. + * + * @param {object} obj + * @param {string} obj.type + * @param {object} [obj.parameters] + * @return {string} + * @private + */ + +function format (obj) { + var parameters = obj.parameters + var type = obj.type + + if (!type || typeof type !== 'string' || !TOKEN_REGEXP.test(type)) { + throw new TypeError('invalid type') + } + + // start with normalized type + var string = String(type).toLowerCase() + + // append parameters + if (parameters && typeof parameters === 'object') { + var param + var params = Object.keys(parameters).sort() + + for (var i = 0; i < params.length; i++) { + param = params[i] + + var val = param.slice(-1) === '*' + ? ustring(parameters[param]) + : qstring(parameters[param]) + + string += '; ' + param + '=' + val + } + } + + return string +} + +/** + * Decode a RFC 5987 field value (gracefully). + * + * @param {string} str + * @return {string} + * @private + */ + +function decodefield (str) { + var match = EXT_VALUE_REGEXP.exec(str) + + if (!match) { + throw new TypeError('invalid extended field value') + } + + var charset = match[1].toLowerCase() + var encoded = match[2] + var value + + // to binary string + var binary = encoded.replace(HEX_ESCAPE_REPLACE_REGEXP, pdecode) + + switch (charset) { + case 'iso-8859-1': + value = getlatin1(binary) + break + case 'utf-8': + case 'utf8': + value = Buffer.from(binary, 'binary').toString('utf8') + break + default: + throw new TypeError('unsupported charset in extended field') + } + + return value +} + +/** + * Get ISO-8859-1 version of string. + * + * @param {string} val + * @return {string} + * @private + */ + +function getlatin1 (val) { + // simple Unicode -> ISO-8859-1 transformation + return String(val).replace(NON_LATIN1_REGEXP, '?') +} + +/** + * Parse Content-Disposition header string. + * + * @param {string} string + * @return {object} + * @public + */ + +function parse (string) { + if (!string || typeof string !== 'string') { + throw new TypeError('argument string is required') + } + + var match = DISPOSITION_TYPE_REGEXP.exec(string) + + if (!match) { + throw new TypeError('invalid type format') + } + + // normalize type + var index = match[0].length + var type = match[1].toLowerCase() + + var key + var names = [] + var params = {} + var value + + // calculate index to start at + index = PARAM_REGEXP.lastIndex = match[0].slice(-1) === ';' + ? index - 1 + : index + + // match parameters + while ((match = PARAM_REGEXP.exec(string))) { + if (match.index !== index) { + throw new TypeError('invalid parameter format') + } + + index += match[0].length + key = match[1].toLowerCase() + value = match[2] + + if (names.indexOf(key) !== -1) { + throw new TypeError('invalid duplicate parameter') + } + + names.push(key) + + if (key.indexOf('*') + 1 === key.length) { + // decode extended value + key = key.slice(0, -1) + value = decodefield(value) + + // overwrite existing value + params[key] = value + continue + } + + if (typeof params[key] === 'string') { + continue + } + + if (value[0] === '"') { + // remove quotes and escapes + value = value + .slice(1, -1) + .replace(QESC_REGEXP, '$1') + } + + params[key] = value + } + + if (index !== -1 && index !== string.length) { + throw new TypeError('invalid parameter format') + } + + return new ContentDisposition(type, params) +} + +/** + * Percent decode a single character. + * + * @param {string} str + * @param {string} hex + * @return {string} + * @private + */ + +function pdecode (str, hex) { + return String.fromCharCode(parseInt(hex, 16)) +} + +/** + * Percent encode a single character. + * + * @param {string} char + * @return {string} + * @private + */ + +function pencode (char) { + return '%' + String(char) + .charCodeAt(0) + .toString(16) + .toUpperCase() +} + +/** + * Quote a string for HTTP. + * + * @param {string} val + * @return {string} + * @private + */ + +function qstring (val) { + var str = String(val) + + return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"' +} + +/** + * Encode a Unicode string for HTTP (RFC 5987). + * + * @param {string} val + * @return {string} + * @private + */ + +function ustring (val) { + var str = String(val) + + // percent encode as UTF-8 + var encoded = encodeURIComponent(str) + .replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode) + + return 'UTF-8\'\'' + encoded +} + +/** + * Class for parsed Content-Disposition header for v8 optimization + * + * @public + * @param {string} type + * @param {object} parameters + * @constructor + */ + +function ContentDisposition (type, parameters) { + this.type = type + this.parameters = parameters +} diff --git a/backend/bridge/node_modules/content-disposition/package.json b/backend/bridge/node_modules/content-disposition/package.json new file mode 100644 index 00000000..a44034cf --- /dev/null +++ b/backend/bridge/node_modules/content-disposition/package.json @@ -0,0 +1,43 @@ +{ + "name": "content-disposition", + "description": "Create and parse Content-Disposition header", + "version": "1.0.1", + "author": "Douglas Christopher Wilson ", + "license": "MIT", + "keywords": [ + "content-disposition", + "http", + "rfc6266", + "res" + ], + "repository": "jshttp/content-disposition", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + }, + "devDependencies": { + "c8": "^10.1.2", + "eslint": "7.32.0", + "eslint-config-standard": "13.0.1", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-markdown": "2.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "5.2.0", + "eslint-plugin-standard": "4.1.0" + }, + "files": [ + "LICENSE", + "HISTORY.md", + "README.md", + "index.js" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "lint": "eslint .", + "test": "node --test --test-reporter spec", + "test-ci": "c8 --reporter=lcovonly --reporter=text npm test", + "test-cov": "c8 --reporter=html --reporter=text npm test" + } +} diff --git a/backend/bridge/node_modules/content-type/HISTORY.md b/backend/bridge/node_modules/content-type/HISTORY.md new file mode 100644 index 00000000..45836713 --- /dev/null +++ b/backend/bridge/node_modules/content-type/HISTORY.md @@ -0,0 +1,29 @@ +1.0.5 / 2023-01-29 +================== + + * perf: skip value escaping when unnecessary + +1.0.4 / 2017-09-11 +================== + + * perf: skip parameter parsing when no parameters + +1.0.3 / 2017-09-10 +================== + + * perf: remove argument reassignment + +1.0.2 / 2016-05-09 +================== + + * perf: enable strict mode + +1.0.1 / 2015-02-13 +================== + + * Improve missing `Content-Type` header error message + +1.0.0 / 2015-02-01 +================== + + * Initial implementation, derived from `media-typer@0.3.0` diff --git a/backend/bridge/node_modules/content-type/LICENSE b/backend/bridge/node_modules/content-type/LICENSE new file mode 100644 index 00000000..34b1a2de --- /dev/null +++ b/backend/bridge/node_modules/content-type/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/bridge/node_modules/content-type/README.md b/backend/bridge/node_modules/content-type/README.md new file mode 100644 index 00000000..c1a922a9 --- /dev/null +++ b/backend/bridge/node_modules/content-type/README.md @@ -0,0 +1,94 @@ +# content-type + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-url] +[![Node.js Version][node-image]][node-url] +[![Build Status][ci-image]][ci-url] +[![Coverage Status][coveralls-image]][coveralls-url] + +Create and parse HTTP Content-Type header according to RFC 7231 + +## Installation + +```sh +$ npm install content-type +``` + +## API + +```js +var contentType = require('content-type') +``` + +### contentType.parse(string) + +```js +var obj = contentType.parse('image/svg+xml; charset=utf-8') +``` + +Parse a `Content-Type` header. This will return an object with the following +properties (examples are shown for the string `'image/svg+xml; charset=utf-8'`): + + - `type`: The media type (the type and subtype, always lower case). + Example: `'image/svg+xml'` + + - `parameters`: An object of the parameters in the media type (name of parameter + always lower case). Example: `{charset: 'utf-8'}` + +Throws a `TypeError` if the string is missing or invalid. + +### contentType.parse(req) + +```js +var obj = contentType.parse(req) +``` + +Parse the `Content-Type` header from the given `req`. Short-cut for +`contentType.parse(req.headers['content-type'])`. + +Throws a `TypeError` if the `Content-Type` header is missing or invalid. + +### contentType.parse(res) + +```js +var obj = contentType.parse(res) +``` + +Parse the `Content-Type` header set on the given `res`. Short-cut for +`contentType.parse(res.getHeader('content-type'))`. + +Throws a `TypeError` if the `Content-Type` header is missing or invalid. + +### contentType.format(obj) + +```js +var str = contentType.format({ + type: 'image/svg+xml', + parameters: { charset: 'utf-8' } +}) +``` + +Format an object into a `Content-Type` header. This will return a string of the +content type for the given object with the following properties (examples are +shown that produce the string `'image/svg+xml; charset=utf-8'`): + + - `type`: The media type (will be lower-cased). Example: `'image/svg+xml'` + + - `parameters`: An object of the parameters in the media type (name of the + parameter will be lower-cased). Example: `{charset: 'utf-8'}` + +Throws a `TypeError` if the object contains an invalid type or parameter names. + +## License + +[MIT](LICENSE) + +[ci-image]: https://badgen.net/github/checks/jshttp/content-type/master?label=ci +[ci-url]: https://github.com/jshttp/content-type/actions/workflows/ci.yml +[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/content-type/master +[coveralls-url]: https://coveralls.io/r/jshttp/content-type?branch=master +[node-image]: https://badgen.net/npm/node/content-type +[node-url]: https://nodejs.org/en/download +[npm-downloads-image]: https://badgen.net/npm/dm/content-type +[npm-url]: https://npmjs.org/package/content-type +[npm-version-image]: https://badgen.net/npm/v/content-type diff --git a/backend/bridge/node_modules/content-type/index.js b/backend/bridge/node_modules/content-type/index.js new file mode 100644 index 00000000..41840e7b --- /dev/null +++ b/backend/bridge/node_modules/content-type/index.js @@ -0,0 +1,225 @@ +/*! + * content-type + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1 + * + * parameter = token "=" ( token / quoted-string ) + * token = 1*tchar + * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + * / DIGIT / ALPHA + * ; any VCHAR, except delimiters + * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE + * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text + * obs-text = %x80-FF + * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + */ +var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g // eslint-disable-line no-control-regex +var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line no-control-regex +var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/ + +/** + * RegExp to match quoted-pair in RFC 7230 sec 3.2.6 + * + * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + * obs-text = %x80-FF + */ +var QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g // eslint-disable-line no-control-regex + +/** + * RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6 + */ +var QUOTE_REGEXP = /([\\"])/g + +/** + * RegExp to match type in RFC 7231 sec 3.1.1.1 + * + * media-type = type "/" subtype + * type = token + * subtype = token + */ +var TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/ + +/** + * Module exports. + * @public + */ + +exports.format = format +exports.parse = parse + +/** + * Format object to media type. + * + * @param {object} obj + * @return {string} + * @public + */ + +function format (obj) { + if (!obj || typeof obj !== 'object') { + throw new TypeError('argument obj is required') + } + + var parameters = obj.parameters + var type = obj.type + + if (!type || !TYPE_REGEXP.test(type)) { + throw new TypeError('invalid type') + } + + var string = type + + // append parameters + if (parameters && typeof parameters === 'object') { + var param + var params = Object.keys(parameters).sort() + + for (var i = 0; i < params.length; i++) { + param = params[i] + + if (!TOKEN_REGEXP.test(param)) { + throw new TypeError('invalid parameter name') + } + + string += '; ' + param + '=' + qstring(parameters[param]) + } + } + + return string +} + +/** + * Parse media type to object. + * + * @param {string|object} string + * @return {Object} + * @public + */ + +function parse (string) { + if (!string) { + throw new TypeError('argument string is required') + } + + // support req/res-like objects as argument + var header = typeof string === 'object' + ? getcontenttype(string) + : string + + if (typeof header !== 'string') { + throw new TypeError('argument string is required to be a string') + } + + var index = header.indexOf(';') + var type = index !== -1 + ? header.slice(0, index).trim() + : header.trim() + + if (!TYPE_REGEXP.test(type)) { + throw new TypeError('invalid media type') + } + + var obj = new ContentType(type.toLowerCase()) + + // parse parameters + if (index !== -1) { + var key + var match + var value + + PARAM_REGEXP.lastIndex = index + + while ((match = PARAM_REGEXP.exec(header))) { + if (match.index !== index) { + throw new TypeError('invalid parameter format') + } + + index += match[0].length + key = match[1].toLowerCase() + value = match[2] + + if (value.charCodeAt(0) === 0x22 /* " */) { + // remove quotes + value = value.slice(1, -1) + + // remove escapes + if (value.indexOf('\\') !== -1) { + value = value.replace(QESC_REGEXP, '$1') + } + } + + obj.parameters[key] = value + } + + if (index !== header.length) { + throw new TypeError('invalid parameter format') + } + } + + return obj +} + +/** + * Get content-type from req/res objects. + * + * @param {object} + * @return {Object} + * @private + */ + +function getcontenttype (obj) { + var header + + if (typeof obj.getHeader === 'function') { + // res-like + header = obj.getHeader('content-type') + } else if (typeof obj.headers === 'object') { + // req-like + header = obj.headers && obj.headers['content-type'] + } + + if (typeof header !== 'string') { + throw new TypeError('content-type header is missing from object') + } + + return header +} + +/** + * Quote a string if necessary. + * + * @param {string} val + * @return {string} + * @private + */ + +function qstring (val) { + var str = String(val) + + // no need to quote tokens + if (TOKEN_REGEXP.test(str)) { + return str + } + + if (str.length > 0 && !TEXT_REGEXP.test(str)) { + throw new TypeError('invalid parameter value') + } + + return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"' +} + +/** + * Class to represent a content type. + * @private + */ +function ContentType (type) { + this.parameters = Object.create(null) + this.type = type +} diff --git a/backend/bridge/node_modules/content-type/package.json b/backend/bridge/node_modules/content-type/package.json new file mode 100644 index 00000000..9db19f63 --- /dev/null +++ b/backend/bridge/node_modules/content-type/package.json @@ -0,0 +1,42 @@ +{ + "name": "content-type", + "description": "Create and parse HTTP Content-Type header", + "version": "1.0.5", + "author": "Douglas Christopher Wilson ", + "license": "MIT", + "keywords": [ + "content-type", + "http", + "req", + "res", + "rfc7231" + ], + "repository": "jshttp/content-type", + "devDependencies": { + "deep-equal": "1.0.1", + "eslint": "8.32.0", + "eslint-config-standard": "15.0.1", + "eslint-plugin-import": "2.27.5", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "6.1.1", + "eslint-plugin-standard": "4.1.0", + "mocha": "10.2.0", + "nyc": "15.1.0" + }, + "files": [ + "LICENSE", + "HISTORY.md", + "README.md", + "index.js" + ], + "engines": { + "node": ">= 0.6" + }, + "scripts": { + "lint": "eslint .", + "test": "mocha --reporter spec --check-leaks --bail test/", + "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test", + "version": "node scripts/version-history.js && git add HISTORY.md" + } +} diff --git a/backend/bridge/node_modules/cookie-signature/History.md b/backend/bridge/node_modules/cookie-signature/History.md new file mode 100644 index 00000000..479211a7 --- /dev/null +++ b/backend/bridge/node_modules/cookie-signature/History.md @@ -0,0 +1,70 @@ +1.2.2 / 2024-10-29 +================== + +* various metadata/documentation tweaks (incl. #51) + + +1.2.1 / 2023-02-27 +================== + +* update annotations for allowed secret key types (#44, thanks @jyasskin!) + + +1.2.0 / 2022-02-17 +================== + +* allow buffer and other node-supported types as key (#33) +* be pickier about extra content after signed portion (#40) +* some internal code clarity/cleanup improvements (#26) + + +1.1.0 / 2018-01-18 +================== + +* switch to built-in `crypto.timingSafeEqual` for validation instead of previous double-hash method (thank you @jodevsa!) + + +1.0.7 / 2023-04-12 +================== + +Later release for older node.js versions. See the [v1.0.x branch notes](https://github.com/tj/node-cookie-signature/blob/v1.0.x/History.md#107--2023-04-12). + + +1.0.6 / 2015-02-03 +================== + +* use `npm test` instead of `make test` to run tests +* clearer assertion messages when checking input + + +1.0.5 / 2014-09-05 +================== + +* add license to package.json + +1.0.4 / 2014-06-25 +================== + + * corrected avoidance of timing attacks (thanks @tenbits!) + +1.0.3 / 2014-01-28 +================== + + * [incorrect] fix for timing attacks + +1.0.2 / 2014-01-28 +================== + + * fix missing repository warning + * fix typo in test + +1.0.1 / 2013-04-15 +================== + + * Revert "Changed underlying HMAC algo. to sha512." + * Revert "Fix for timing attacks on MAC verification." + +0.0.1 / 2010-01-03 +================== + + * Initial release diff --git a/backend/bridge/node_modules/cookie-signature/LICENSE b/backend/bridge/node_modules/cookie-signature/LICENSE new file mode 100644 index 00000000..a2671bf7 --- /dev/null +++ b/backend/bridge/node_modules/cookie-signature/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2012–2024 LearnBoost and other contributors; + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/bridge/node_modules/cookie-signature/Readme.md b/backend/bridge/node_modules/cookie-signature/Readme.md new file mode 100644 index 00000000..369af15f --- /dev/null +++ b/backend/bridge/node_modules/cookie-signature/Readme.md @@ -0,0 +1,23 @@ + +# cookie-signature + + Sign and unsign cookies. + +## Example + +```js +var cookie = require('cookie-signature'); + +var val = cookie.sign('hello', 'tobiiscool'); +val.should.equal('hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI'); + +var val = cookie.sign('hello', 'tobiiscool'); +cookie.unsign(val, 'tobiiscool').should.equal('hello'); +cookie.unsign(val, 'luna').should.be.false; +``` + +## License + +MIT. + +See LICENSE file for details. diff --git a/backend/bridge/node_modules/cookie-signature/index.js b/backend/bridge/node_modules/cookie-signature/index.js new file mode 100644 index 00000000..3fbbddb6 --- /dev/null +++ b/backend/bridge/node_modules/cookie-signature/index.js @@ -0,0 +1,47 @@ +/** + * Module dependencies. + */ + +var crypto = require('crypto'); + +/** + * Sign the given `val` with `secret`. + * + * @param {String} val + * @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret + * @return {String} + * @api private + */ + +exports.sign = function(val, secret){ + if ('string' != typeof val) throw new TypeError("Cookie value must be provided as a string."); + if (null == secret) throw new TypeError("Secret key must be provided."); + return val + '.' + crypto + .createHmac('sha256', secret) + .update(val) + .digest('base64') + .replace(/\=+$/, ''); +}; + +/** + * Unsign and decode the given `input` with `secret`, + * returning `false` if the signature is invalid. + * + * @param {String} input + * @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret + * @return {String|Boolean} + * @api private + */ + +exports.unsign = function(input, secret){ + if ('string' != typeof input) throw new TypeError("Signed cookie string must be provided."); + if (null == secret) throw new TypeError("Secret key must be provided."); + var tentativeValue = input.slice(0, input.lastIndexOf('.')), + expectedInput = exports.sign(tentativeValue, secret), + expectedBuffer = Buffer.from(expectedInput), + inputBuffer = Buffer.from(input); + return ( + expectedBuffer.length === inputBuffer.length && + crypto.timingSafeEqual(expectedBuffer, inputBuffer) + ) ? tentativeValue : false; +}; diff --git a/backend/bridge/node_modules/cookie-signature/package.json b/backend/bridge/node_modules/cookie-signature/package.json new file mode 100644 index 00000000..a1600400 --- /dev/null +++ b/backend/bridge/node_modules/cookie-signature/package.json @@ -0,0 +1,24 @@ +{ + "name": "cookie-signature", + "version": "1.2.2", + "main": "index.js", + "description": "Sign and unsign cookies", + "keywords": ["cookie", "sign", "unsign"], + "author": "TJ Holowaychuk ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/visionmedia/node-cookie-signature.git" + }, + "dependencies": {}, + "engines": { + "node": ">=6.6.0" + }, + "devDependencies": { + "mocha": "*", + "should": "*" + }, + "scripts": { + "test": "mocha --require should --reporter spec" + } +} diff --git a/backend/bridge/node_modules/cookie/LICENSE b/backend/bridge/node_modules/cookie/LICENSE new file mode 100644 index 00000000..058b6b4e --- /dev/null +++ b/backend/bridge/node_modules/cookie/LICENSE @@ -0,0 +1,24 @@ +(The MIT License) + +Copyright (c) 2012-2014 Roman Shtylman +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/backend/bridge/node_modules/cookie/README.md b/backend/bridge/node_modules/cookie/README.md new file mode 100644 index 00000000..71fdac11 --- /dev/null +++ b/backend/bridge/node_modules/cookie/README.md @@ -0,0 +1,317 @@ +# cookie + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-url] +[![Node.js Version][node-image]][node-url] +[![Build Status][ci-image]][ci-url] +[![Coverage Status][coveralls-image]][coveralls-url] + +Basic HTTP cookie parser and serializer for HTTP servers. + +## Installation + +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). Installation is done using the +[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): + +```sh +$ npm install cookie +``` + +## API + +```js +var cookie = require('cookie'); +``` + +### cookie.parse(str, options) + +Parse an HTTP `Cookie` header string and returning an object of all cookie name-value pairs. +The `str` argument is the string representing a `Cookie` header value and `options` is an +optional object containing additional parsing options. + +```js +var cookies = cookie.parse('foo=bar; equation=E%3Dmc%5E2'); +// { foo: 'bar', equation: 'E=mc^2' } +``` + +#### Options + +`cookie.parse` accepts these properties in the options object. + +##### decode + +Specifies a function that will be used to decode a cookie's value. Since the value of a cookie +has a limited character set (and must be a simple string), this function can be used to decode +a previously-encoded cookie value into a JavaScript string or other object. + +The default function is the global `decodeURIComponent`, which will decode any URL-encoded +sequences into their byte representations. + +**note** if an error is thrown from this function, the original, non-decoded cookie value will +be returned as the cookie's value. + +### cookie.serialize(name, value, options) + +Serialize a cookie name-value pair into a `Set-Cookie` header string. The `name` argument is the +name for the cookie, the `value` argument is the value to set the cookie to, and the `options` +argument is an optional object containing additional serialization options. + +```js +var setCookie = cookie.serialize('foo', 'bar'); +// foo=bar +``` + +#### Options + +`cookie.serialize` accepts these properties in the options object. + +##### domain + +Specifies the value for the [`Domain` `Set-Cookie` attribute][rfc-6265-5.2.3]. By default, no +domain is set, and most clients will consider the cookie to apply to only the current domain. + +##### encode + +Specifies a function that will be used to encode a cookie's value. Since value of a cookie +has a limited character set (and must be a simple string), this function can be used to encode +a value into a string suited for a cookie's value. + +The default function is the global `encodeURIComponent`, which will encode a JavaScript string +into UTF-8 byte sequences and then URL-encode any that fall outside of the cookie range. + +##### expires + +Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute][rfc-6265-5.2.1]. +By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and +will delete it on a condition like exiting a web browser application. + +**note** the [cookie storage model specification][rfc-6265-5.3] states that if both `expires` and +`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this, +so if both are set, they should point to the same date and time. + +##### httpOnly + +Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute][rfc-6265-5.2.6]. When truthy, +the `HttpOnly` attribute is set, otherwise it is not. By default, the `HttpOnly` attribute is not set. + +**note** be careful when setting this to `true`, as compliant clients will not allow client-side +JavaScript to see the cookie in `document.cookie`. + +##### maxAge + +Specifies the `number` (in seconds) to be the value for the [`Max-Age` `Set-Cookie` attribute][rfc-6265-5.2.2]. +The given number will be converted to an integer by rounding down. By default, no maximum age is set. + +**note** the [cookie storage model specification][rfc-6265-5.3] states that if both `expires` and +`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this, +so if both are set, they should point to the same date and time. + +##### partitioned + +Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](rfc-cutler-httpbis-partitioned-cookies) +attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the +`Partitioned` attribute is not set. + +**note** This is an attribute that has not yet been fully standardized, and may change in the future. +This also means many clients may ignore this attribute until they understand it. + +More information about can be found in [the proposal](https://github.com/privacycg/CHIPS). + +##### path + +Specifies the value for the [`Path` `Set-Cookie` attribute][rfc-6265-5.2.4]. By default, the path +is considered the ["default path"][rfc-6265-5.1.4]. + +##### priority + +Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1]. + + - `'low'` will set the `Priority` attribute to `Low`. + - `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set. + - `'high'` will set the `Priority` attribute to `High`. + +More information about the different priority levels can be found in +[the specification][rfc-west-cookie-priority-00-4.1]. + +**note** This is an attribute that has not yet been fully standardized, and may change in the future. +This also means many clients may ignore this attribute until they understand it. + +##### sameSite + +Specifies the `boolean` or `string` to be the value for the [`SameSite` `Set-Cookie` attribute][rfc-6265bis-09-5.4.7]. + + - `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement. + - `false` will not set the `SameSite` attribute. + - `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement. + - `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie. + - `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement. + +More information about the different enforcement levels can be found in +[the specification][rfc-6265bis-09-5.4.7]. + +**note** This is an attribute that has not yet been fully standardized, and may change in the future. +This also means many clients may ignore this attribute until they understand it. + +##### secure + +Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute][rfc-6265-5.2.5]. When truthy, +the `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set. + +**note** be careful when setting this to `true`, as compliant clients will not send the cookie back to +the server in the future if the browser does not have an HTTPS connection. + +## Example + +The following example uses this module in conjunction with the Node.js core HTTP server +to prompt a user for their name and display it back on future visits. + +```js +var cookie = require('cookie'); +var escapeHtml = require('escape-html'); +var http = require('http'); +var url = require('url'); + +function onRequest(req, res) { + // Parse the query string + var query = url.parse(req.url, true, true).query; + + if (query && query.name) { + // Set a new cookie with the name + res.setHeader('Set-Cookie', cookie.serialize('name', String(query.name), { + httpOnly: true, + maxAge: 60 * 60 * 24 * 7 // 1 week + })); + + // Redirect back after setting cookie + res.statusCode = 302; + res.setHeader('Location', req.headers.referer || '/'); + res.end(); + return; + } + + // Parse the cookies on the request + var cookies = cookie.parse(req.headers.cookie || ''); + + // Get the visitor name set in the cookie + var name = cookies.name; + + res.setHeader('Content-Type', 'text/html; charset=UTF-8'); + + if (name) { + res.write('

Welcome back, ' + escapeHtml(name) + '!

'); + } else { + res.write('

Hello, new visitor!

'); + } + + res.write('
'); + res.write(' '); + res.end('
'); +} + +http.createServer(onRequest).listen(3000); +``` + +## Testing + +```sh +$ npm test +``` + +## Benchmark + +``` +$ npm run bench + +> cookie@0.5.0 bench +> node benchmark/index.js + + node@18.18.2 + acorn@8.10.0 + ada@2.6.0 + ares@1.19.1 + brotli@1.0.9 + cldr@43.1 + icu@73.2 + llhttp@6.0.11 + modules@108 + napi@9 + nghttp2@1.57.0 + nghttp3@0.7.0 + ngtcp2@0.8.1 + openssl@3.0.10+quic + simdutf@3.2.14 + tz@2023c + undici@5.26.3 + unicode@15.0 + uv@1.44.2 + uvwasi@0.0.18 + v8@10.2.154.26-node.26 + zlib@1.2.13.1-motley + +> node benchmark/parse-top.js + + cookie.parse - top sites + + 14 tests completed. + + parse accounts.google.com x 2,588,913 ops/sec ±0.74% (186 runs sampled) + parse apple.com x 2,370,002 ops/sec ±0.69% (186 runs sampled) + parse cloudflare.com x 2,213,102 ops/sec ±0.88% (188 runs sampled) + parse docs.google.com x 2,194,157 ops/sec ±1.03% (184 runs sampled) + parse drive.google.com x 2,265,084 ops/sec ±0.79% (187 runs sampled) + parse en.wikipedia.org x 457,099 ops/sec ±0.81% (186 runs sampled) + parse linkedin.com x 504,407 ops/sec ±0.89% (186 runs sampled) + parse maps.google.com x 1,230,959 ops/sec ±0.98% (186 runs sampled) + parse microsoft.com x 926,294 ops/sec ±0.88% (184 runs sampled) + parse play.google.com x 2,311,338 ops/sec ±0.83% (185 runs sampled) + parse support.google.com x 1,508,850 ops/sec ±0.86% (186 runs sampled) + parse www.google.com x 1,022,582 ops/sec ±1.32% (182 runs sampled) + parse youtu.be x 332,136 ops/sec ±1.02% (185 runs sampled) + parse youtube.com x 323,833 ops/sec ±0.77% (183 runs sampled) + +> node benchmark/parse.js + + cookie.parse - generic + + 6 tests completed. + + simple x 3,214,032 ops/sec ±1.61% (183 runs sampled) + decode x 587,237 ops/sec ±1.16% (187 runs sampled) + unquote x 2,954,618 ops/sec ±1.35% (183 runs sampled) + duplicates x 857,008 ops/sec ±0.89% (187 runs sampled) + 10 cookies x 292,133 ops/sec ±0.89% (187 runs sampled) + 100 cookies x 22,610 ops/sec ±0.68% (187 runs sampled) +``` + +## References + +- [RFC 6265: HTTP State Management Mechanism][rfc-6265] +- [Same-site Cookies][rfc-6265bis-09-5.4.7] + +[rfc-cutler-httpbis-partitioned-cookies]: https://tools.ietf.org/html/draft-cutler-httpbis-partitioned-cookies/ +[rfc-west-cookie-priority-00-4.1]: https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1 +[rfc-6265bis-09-5.4.7]: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7 +[rfc-6265]: https://tools.ietf.org/html/rfc6265 +[rfc-6265-5.1.4]: https://tools.ietf.org/html/rfc6265#section-5.1.4 +[rfc-6265-5.2.1]: https://tools.ietf.org/html/rfc6265#section-5.2.1 +[rfc-6265-5.2.2]: https://tools.ietf.org/html/rfc6265#section-5.2.2 +[rfc-6265-5.2.3]: https://tools.ietf.org/html/rfc6265#section-5.2.3 +[rfc-6265-5.2.4]: https://tools.ietf.org/html/rfc6265#section-5.2.4 +[rfc-6265-5.2.5]: https://tools.ietf.org/html/rfc6265#section-5.2.5 +[rfc-6265-5.2.6]: https://tools.ietf.org/html/rfc6265#section-5.2.6 +[rfc-6265-5.3]: https://tools.ietf.org/html/rfc6265#section-5.3 + +## License + +[MIT](LICENSE) + +[ci-image]: https://badgen.net/github/checks/jshttp/cookie/master?label=ci +[ci-url]: https://github.com/jshttp/cookie/actions/workflows/ci.yml +[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/cookie/master +[coveralls-url]: https://coveralls.io/r/jshttp/cookie?branch=master +[node-image]: https://badgen.net/npm/node/cookie +[node-url]: https://nodejs.org/en/download +[npm-downloads-image]: https://badgen.net/npm/dm/cookie +[npm-url]: https://npmjs.org/package/cookie +[npm-version-image]: https://badgen.net/npm/v/cookie diff --git a/backend/bridge/node_modules/cookie/SECURITY.md b/backend/bridge/node_modules/cookie/SECURITY.md new file mode 100644 index 00000000..fd4a6c53 --- /dev/null +++ b/backend/bridge/node_modules/cookie/SECURITY.md @@ -0,0 +1,25 @@ +# Security Policies and Procedures + +## Reporting a Bug + +The `cookie` team and community take all security bugs seriously. Thank +you for improving the security of the project. We appreciate your efforts and +responsible disclosure and will make every effort to acknowledge your +contributions. + +Report security bugs by emailing the current owner(s) of `cookie`. This +information can be found in the npm registry using the command +`npm owner ls cookie`. +If unsure or unable to get the information from the above, open an issue +in the [project issue tracker](https://github.com/jshttp/cookie/issues) +asking for the current contact information. + +To ensure the timely response to your report, please ensure that the entirety +of the report is contained within the email body and not solely behind a web +link or an attachment. + +At least one owner will acknowledge your email within 48 hours, and will send a +more detailed response within 48 hours indicating the next steps in handling +your report. After the initial reply to your report, the owners will +endeavor to keep you informed of the progress towards a fix and full +announcement, and may ask for additional information or guidance. diff --git a/backend/bridge/node_modules/cookie/index.js b/backend/bridge/node_modules/cookie/index.js new file mode 100644 index 00000000..acd5acd6 --- /dev/null +++ b/backend/bridge/node_modules/cookie/index.js @@ -0,0 +1,335 @@ +/*! + * cookie + * Copyright(c) 2012-2014 Roman Shtylman + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module exports. + * @public + */ + +exports.parse = parse; +exports.serialize = serialize; + +/** + * Module variables. + * @private + */ + +var __toString = Object.prototype.toString +var __hasOwnProperty = Object.prototype.hasOwnProperty + +/** + * RegExp to match cookie-name in RFC 6265 sec 4.1.1 + * This refers out to the obsoleted definition of token in RFC 2616 sec 2.2 + * which has been replaced by the token definition in RFC 7230 appendix B. + * + * cookie-name = token + * token = 1*tchar + * tchar = "!" / "#" / "$" / "%" / "&" / "'" / + * "*" / "+" / "-" / "." / "^" / "_" / + * "`" / "|" / "~" / DIGIT / ALPHA + */ + +var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; + +/** + * RegExp to match cookie-value in RFC 6265 sec 4.1.1 + * + * cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) + * cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + * ; US-ASCII characters excluding CTLs, + * ; whitespace DQUOTE, comma, semicolon, + * ; and backslash + */ + +var cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/; + +/** + * RegExp to match domain-value in RFC 6265 sec 4.1.1 + * + * domain-value = + * ; defined in [RFC1034], Section 3.5, as + * ; enhanced by [RFC1123], Section 2.1 + * =