Merge branch 'main' of https://github.com/qinfendebingshuo/guanghulab
This commit is contained in:
commit
2a77292997
|
|
@ -0,0 +1,72 @@
|
|||
name: 铸渊 · Bridge E · GitHub Changes → Notion
|
||||
|
||||
# 管道 E:main 分支有 push 或 PR 创建/关闭 → 变更摘要同步到 Notion「📋 GitHub 变更日志」数据库
|
||||
#
|
||||
# 依赖 Secrets(仓库 Settings → Secrets and variables → Actions):
|
||||
# NOTION_TOKEN Notion 集成 token
|
||||
#
|
||||
# CHANGES_DB_ID 已内置默认值(e740b77aa6bd4ac0a2e8a75f678fba98)
|
||||
# 如需覆盖,可在 Secrets 中添加 CHANGES_DB_ID
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, closed]
|
||||
|
||||
jobs:
|
||||
bridge-changes-to-notion:
|
||||
name: 📡 Bridge E · GitHub Changes → Notion
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2 # 获取前一次 commit 以便 diff
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
# ── commit push ──────────────────────────────────────
|
||||
- name: 📡 同步 commit 变更到 Notion
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
|
||||
CHANGES_DB_ID: ${{ secrets.CHANGES_DB_ID }}
|
||||
EVENT_TYPE: commit
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
||||
COMMITTER: ${{ github.event.head_commit.author.name || github.actor }}
|
||||
COMMIT_TIMESTAMP: ${{ github.event.head_commit.timestamp }}
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
run: |
|
||||
# git diff 输出换行分隔的变更文件列表(包含新增、修改、删除)
|
||||
# COMMIT_TIMESTAMP/COMMITTER fallback: GitHub 表达式 || 在空字符串时也会取右侧值
|
||||
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || true)
|
||||
export CHANGED_FILES="$CHANGED"
|
||||
node scripts/notion-bridge.js changes
|
||||
|
||||
# ── PR 事件 ───────────────────────────────────────────
|
||||
- name: 📡 同步 PR 变更到 Notion
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
|
||||
CHANGES_DB_ID: ${{ secrets.CHANGES_DB_ID }}
|
||||
EVENT_TYPE: pr
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_ACTION: ${{ github.event.action }}
|
||||
PR_MERGED: ${{ github.event.pull_request.merged }}
|
||||
COMMITTER: ${{ github.event.pull_request.user.login }}
|
||||
COMMIT_TIMESTAMP: ${{ github.event.pull_request.updated_at }}
|
||||
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
COMMIT_MSG: ${{ github.event.pull_request.title }}
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
run: node scripts/notion-bridge.js changes
|
||||
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
name: 铸渊 · Bridge A · SYSLOG → Notion
|
||||
|
||||
# 管道 A:syslog-inbox/ 有新文件 push → 同步到 Notion「📥 GitHub SYSLOG 收件箱」数据库
|
||||
#
|
||||
# 依赖 Secrets(仓库 Settings → Secrets and variables → Actions):
|
||||
# NOTION_TOKEN Notion 集成 token
|
||||
#
|
||||
# SYSLOG_DB_ID 已内置默认值(330ab17507d542c9bbb96d0749b41197)
|
||||
# 如需覆盖,可在 Secrets 中添加 SYSLOG_DB_ID
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'syslog-inbox/**'
|
||||
- 'syslog-processed/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
bridge-syslog-to-notion:
|
||||
name: 📥 Bridge A · SYSLOG → Notion
|
||||
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: 🔗 同步 syslog-inbox 到 Notion
|
||||
env:
|
||||
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
|
||||
SYSLOG_DB_ID: ${{ secrets.SYSLOG_DB_ID }}
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
run: node scripts/notion-bridge.js syslog
|
||||
|
|
@ -2,7 +2,9 @@ name: 🌀 部署铸渊聊天室 (GitHub Pages)
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- main
|
||||
- copilot/** # Copilot Agent 功能分支可直接部署预览,PR 合并后自动失效
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/brain/**'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
name: 铸渊 · SYSLOG Pipeline (A/D/E)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'syslog-inbox/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# ── Pipeline A:SYSLOG 读取与处理 ──────────────────────
|
||||
pipeline-a-syslog:
|
||||
name: 📥 Pipeline A · SYSLOG 读取处理
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
issues: read
|
||||
outputs:
|
||||
processed_count: ${{ steps.process.outputs.processed_count }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: main
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: 📥 处理 syslog-inbox 条目
|
||||
id: process
|
||||
run: |
|
||||
node scripts/process-syslog.js
|
||||
COUNT=$(ls syslog-processed/ -R 2>/dev/null | grep -c '\.json' || echo 0)
|
||||
echo "processed_count=$COUNT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 提交处理结果
|
||||
run: |
|
||||
git config user.name "铸渊[bot]"
|
||||
git config user.email "zhu-yuan-bot@guanghulab.com"
|
||||
git add syslog-inbox/ syslog-processed/ .github/brain/memory.json
|
||||
git diff --staged --quiet || git commit -m "📥 syslog: 处理 inbox 条目,更新大脑记忆 [skip ci]"
|
||||
git push
|
||||
|
||||
# ── Pipeline D:Issues 巡检 ─────────────────────────────
|
||||
pipeline-d-issues:
|
||||
name: 🔍 Pipeline D · Issues 巡检
|
||||
runs-on: ubuntu-latest
|
||||
needs: pipeline-a-syslog
|
||||
permissions:
|
||||
contents: write
|
||||
issues: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: main
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: 🔍 巡检 Issues
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
echo "🔍 Pipeline D · Issues 巡检开始"
|
||||
node -e "
|
||||
const https = require('https');
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
const repo = process.env.REPO;
|
||||
const opts = {
|
||||
hostname: 'api.github.com',
|
||||
path: '/repos/' + repo + '/issues?state=open&per_page=20',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'User-Agent': 'zhuyuan-bot' }
|
||||
};
|
||||
https.get(opts, res => {
|
||||
let body = '';
|
||||
res.on('data', c => body += c);
|
||||
res.on('end', () => {
|
||||
const issues = JSON.parse(body);
|
||||
console.log('📋 开放 Issues 数量:' + (Array.isArray(issues) ? issues.length : '读取失败'));
|
||||
if (Array.isArray(issues) && issues.length > 0) {
|
||||
issues.slice(0, 5).forEach(i => console.log(' #' + i.number + ' ' + i.title));
|
||||
}
|
||||
console.log('✅ Pipeline D 巡检完成');
|
||||
});
|
||||
}).on('error', e => { console.error('❌ 巡检失败:', e.message); process.exit(1); });
|
||||
"
|
||||
|
||||
# ── Pipeline E:变更感知 ────────────────────────────────
|
||||
pipeline-e-changes:
|
||||
name: 📡 Pipeline E · 变更感知
|
||||
runs-on: ubuntu-latest
|
||||
needs: pipeline-a-syslog
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: main
|
||||
fetch-depth: 10
|
||||
|
||||
- name: 📡 感知近期变更
|
||||
run: |
|
||||
echo "📡 Pipeline E · 变更感知开始"
|
||||
echo "=== 近期 10 次提交 ==="
|
||||
git --no-pager log --oneline -10
|
||||
echo ""
|
||||
echo "=== syslog-inbox 文件状态 ==="
|
||||
ls -la syslog-inbox/ || echo "(空)"
|
||||
echo ""
|
||||
echo "=== syslog-processed 文件状态 ==="
|
||||
ls -laR syslog-processed/ || echo "(空)"
|
||||
echo "✅ Pipeline E 变更感知完成"
|
||||
14
README.md
14
README.md
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
**一句话:点下面这个链接就能打开,直接聊天。**
|
||||
|
||||
### 👉 [点这里打开铸渊聊天室](https://qinfendebingshuo.github.io/guanghulab/)
|
||||
### 👉 [点这里打开铸渊聊天室](https://qinfendebingshuo.github.io/guanghulab/docs/index.html)
|
||||
|
||||
```
|
||||
https://qinfendebingshuo.github.io/guanghulab/
|
||||
https://qinfendebingshuo.github.io/guanghulab/docs/index.html
|
||||
```
|
||||
|
||||
> 把这个链接发给任何人,他们打开就能用,不需要安装任何东西。
|
||||
|
|
@ -20,7 +20,7 @@ https://qinfendebingshuo.github.io/guanghulab/
|
|||
|
||||
### ❄️ 冰朔(你)
|
||||
|
||||
1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/
|
||||
1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/docs/index.html
|
||||
2. **选身份** → 下拉菜单选「❄️ 冰朔(语言架构师·创始人)」
|
||||
3. **填 API 密钥** → 输入你的云雾/OpenAI/DeepSeek API Key
|
||||
4. **点「开始对话」** → 铸渊自动唤醒,汇报大脑状态
|
||||
|
|
@ -30,7 +30,7 @@ https://qinfendebingshuo.github.io/guanghulab/
|
|||
|
||||
### 🦁 肥猫(光湖团队总控 · DEV-002)
|
||||
|
||||
1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/
|
||||
1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/docs/index.html
|
||||
2. **选身份** → 下拉菜单选「🦁 肥猫(光湖团队总控·DEV-002)」
|
||||
3. **填 API 密钥** → 输入自己的 API Key(或点「演示模式」也能看进度面板)
|
||||
4. **点「开始对话」** → 铸渊自动打开**总控指挥台**,显示全员进度
|
||||
|
|
@ -44,7 +44,7 @@ https://qinfendebingshuo.github.io/guanghulab/
|
|||
|
||||
### 🍊 桔子(光湖主控 · DEV-010)
|
||||
|
||||
1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/
|
||||
1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/docs/index.html
|
||||
2. **选身份** → 下拉菜单选「🍊 桔子(光湖主控·DEV-010)」
|
||||
3. **填 API 密钥** → 输入自己的 API Key
|
||||
4. **点「开始对话」** → 铸渊显示你当前模块进度,并开放指挥台
|
||||
|
|
@ -53,7 +53,7 @@ https://qinfendebingshuo.github.io/guanghulab/
|
|||
|
||||
### 👩💻 其他开发者(页页 / 燕樊 / 小草莓 / 花尔 等)
|
||||
|
||||
1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/
|
||||
1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/docs/index.html
|
||||
2. **选身份** → 下拉菜单选自己的名字
|
||||
3. **填 API 密钥** → 填自己的 Key(没有的话点「演示模式」)
|
||||
4. **点「开始对话」** → 铸渊显示你的当前任务状态,可以问问题
|
||||
|
|
@ -80,7 +80,7 @@ https://qinfendebingshuo.github.io/guanghulab/
|
|||
|
||||
```
|
||||
铸渊聊天室入口 👇
|
||||
https://qinfendebingshuo.github.io/guanghulab/
|
||||
https://qinfendebingshuo.github.io/guanghulab/docs/index.html
|
||||
|
||||
打开后选择你的身份,填入 API Key 就能和铸渊说话了。
|
||||
```
|
||||
|
|
|
|||
300
docs/index.html
300
docs/index.html
|
|
@ -1,8 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<!-- 铸渊助手 v3.1 · 2026-03-06 · auto-detect API + Gemini + sessionStorage key -->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<title>铸渊助手 · HoloLake AI</title>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
|
|
@ -136,6 +140,9 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
|
|||
.sfg small{display:block;color:var(--dim);font-size:11px;margin-top:4px}
|
||||
.sbtn2{width:100%;padding:10px;background:var(--accent);color:#fff;border:none;border-radius:var(--rs);font-size:14px;cursor:pointer;transition:opacity .2s;font-family:inherit}
|
||||
.sbtn2:hover{opacity:.85}
|
||||
.det-btn{flex-shrink:0;padding:0 12px;border-radius:var(--rs);border:1px solid var(--accent);background:transparent;color:var(--accent);font-size:12px;cursor:pointer;white-space:nowrap;transition:all .2s;font-family:inherit}
|
||||
.det-btn:hover{background:rgba(79,142,247,.1)}
|
||||
.det-btn:disabled{opacity:.5;cursor:default}
|
||||
.sec-ttl{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin:18px 0 10px;border-top:1px solid var(--border);padding-top:12px}
|
||||
.ir2{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid rgba(30,50,81,.5);font-size:13px}
|
||||
.iv{color:var(--ok);font-weight:500}
|
||||
|
|
@ -218,12 +225,14 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
|
|||
<div class="sc-logo">🌀</div>
|
||||
<h1>铸渊助手</h1>
|
||||
<p class="sub">HoloLake · 代码守护人格体 · 持续成长的 AI 伙伴</p>
|
||||
<div class="sc-sec">🔒 API 密钥只保存在你的浏览器本地,不会上传至任何服务器</div>
|
||||
<div class="sc-sec">🔒 API 密钥仅本次会话有效,关闭标签页即自动清除,不会持久保存在任何地方</div>
|
||||
|
||||
<div class="fg">
|
||||
<label>选择 AI 提供商</label>
|
||||
<select id="sp" onchange="onProv(this.value,'s')">
|
||||
<option value="yunwu">☁️ 云雾 AI(团队推荐)</option>
|
||||
<option value="openai">OpenAI (ChatGPT)</option>
|
||||
<option value="gemini">🔮 Google Gemini</option>
|
||||
<option value="deepseek">DeepSeek(深度求索)</option>
|
||||
<option value="moonshot">Moonshot(Kimi)</option>
|
||||
<option value="zhipu">智谱 AI(GLM)</option>
|
||||
|
|
@ -237,11 +246,17 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
|
|||
<div class="fg">
|
||||
<label>模型</label>
|
||||
<select id="sm"></select>
|
||||
<input type="text" id="sm-cust" placeholder="输入模型名称,如 gpt-4o、claude-3-5-sonnet-20241022、deepseek-chat" style="display:none;margin-top:6px">
|
||||
<small id="sm-cust-hint" style="display:none;color:var(--dim);font-size:11px;margin-top:4px">自定义模式:直接输入你的服务商支持的任意模型名称</small>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>API 密钥</label>
|
||||
<input type="password" id="sk" placeholder="sk-… 或对应格式的密钥">
|
||||
<div style="display:flex;gap:6px;align-items:stretch">
|
||||
<input type="password" id="sk" placeholder="粘贴任意 API 密钥,点击右侧按钮自动识别提供商和模型" style="flex:1">
|
||||
<button onclick="autoDetectAPI('s')" id="sdet-btn" class="det-btn" title="自动检测提供商和可用模型">🔍 自动检测</button>
|
||||
</div>
|
||||
<small id="sk-err" style="color:var(--err);display:none">⚠️ 请输入 API 密钥</small>
|
||||
<small id="sdet-status" style="display:block;margin-top:4px;min-height:16px"></small>
|
||||
</div>
|
||||
|
||||
<div class="id-section">
|
||||
|
|
@ -336,12 +351,20 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
|
|||
|
||||
<!-- ════════════════════ SETTINGS PANEL ════════════════════ -->
|
||||
<div id="sp2" class="sp">
|
||||
<div class="ph"><h2>⚙️ API 设置</h2><button class="cb" onclick="closeP()">✕</button></div>
|
||||
<div class="ph">
|
||||
<h2>⚙️ API 设置</h2>
|
||||
<div style="display:flex;gap:6px;align-items:center">
|
||||
<button onclick="resetAllSettings()" title="清除所有本地设置,重新开始" style="padding:4px 10px;border-radius:6px;border:1px solid #ef4444;background:transparent;color:#ef4444;font-size:11px;cursor:pointer;white-space:nowrap">🔄 重置</button>
|
||||
<button class="cb" onclick="closeP()">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pb">
|
||||
<div class="sfg">
|
||||
<label>AI 提供商</label>
|
||||
<select id="cp" onchange="onProv(this.value,'c')">
|
||||
<option value="yunwu">☁️ 云雾 AI(团队推荐)</option>
|
||||
<option value="openai">OpenAI (ChatGPT)</option>
|
||||
<option value="gemini">🔮 Google Gemini</option>
|
||||
<option value="deepseek">DeepSeek(深度求索)</option>
|
||||
<option value="moonshot">Moonshot(Kimi)</option>
|
||||
<option value="zhipu">智谱 AI(GLM)</option>
|
||||
|
|
@ -355,11 +378,17 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
|
|||
<div class="sfg">
|
||||
<label>模型</label>
|
||||
<select id="cm"></select>
|
||||
<input type="text" id="cm-cust" placeholder="输入模型名称,如 gpt-4o、claude-3-5-sonnet-20241022、deepseek-chat" style="display:none;margin-top:6px">
|
||||
<small id="cm-cust-hint" style="display:none;color:var(--dim);font-size:11px;margin-top:4px">自定义模式:直接输入你的服务商支持的任意模型名称</small>
|
||||
</div>
|
||||
<div class="sfg">
|
||||
<label>API 密钥</label>
|
||||
<input type="password" id="ck" placeholder="sk-…">
|
||||
<small>🔒 只保存在浏览器 localStorage,不上传任何服务器</small>
|
||||
<div style="display:flex;gap:6px;align-items:stretch">
|
||||
<input type="password" id="ck" placeholder="留空 = 保持当前密钥不变;粘贴新密钥即替换" style="flex:1">
|
||||
<button onclick="autoDetectAPI('c')" id="cdet-btn" class="det-btn" title="自动检测提供商和可用模型">🔍 自动检测</button>
|
||||
</div>
|
||||
<small id="ck-hint" style="color:var(--dim)">🔒 密钥仅本次会话有效,关闭标签页自动清除</small>
|
||||
<small id="cdet-status" style="display:block;margin-top:4px;min-height:14px"></small>
|
||||
</div>
|
||||
<button class="sbtn2" onclick="saveSet()">💾 保存设置</button>
|
||||
|
||||
|
|
@ -399,6 +428,10 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
|
|||
4. 点 ⬇️ 下载单文件,放桌面直接打开使用<br>
|
||||
5. 支持 OpenAI · DeepSeek · Kimi · 智谱 · 任意兼容接口
|
||||
</p>
|
||||
|
||||
<div class="sec-ttl" style="color:#ef4444;margin-top:16px">⚠️ 危险操作</div>
|
||||
<p style="font-size:12px;color:var(--dim);margin-bottom:8px">遇到密钥卡住、无法更新等问题时,点下方按钮彻底清除本地设置,从头重新输入。</p>
|
||||
<button onclick="resetAllSettings()" style="width:100%;padding:10px;border-radius:8px;border:1px solid #ef4444;background:transparent;color:#ef4444;font-size:13px;cursor:pointer">🔄 清除所有设置,重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -426,7 +459,7 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
|
|||
<ol class="help-steps">
|
||||
<li data-n="1" id="helpUrlStep">打开链接 <strong>https://qinfendebingshuo.github.io/guanghulab/</strong></li>
|
||||
<li data-n="2"><strong>下拉菜单选择你是谁</strong>(冰朔 / 肥猫 / 桔子 / 开发者名字)</li>
|
||||
<li data-n="3">填入你的 <strong>API Key</strong>(没有的话点「演示模式」)</li>
|
||||
<li data-n="3">填入你的 <strong>API Key</strong> 并点「🔍 自动检测」,系统将自动识别提供商和可用模型</li>
|
||||
<li data-n="4">点「开始对话」,铸渊会针对你的身份打招呼</li>
|
||||
<li data-n="5">直接用聊天的方式问铸渊任何问题</li>
|
||||
</ol>
|
||||
|
|
@ -435,8 +468,8 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
|
|||
<div class="help-section">
|
||||
<h3>🔑 从哪里获取 API Key?</h3>
|
||||
<div class="role-guide">
|
||||
<div class="role-guide-title">推荐用云雾 API(团队共用)</div>
|
||||
<p>找冰朔或肥猫获取团队 Key。填入后选「自定义」提供商,或直接用 DeepSeek。</p>
|
||||
<div class="role-guide-title">推荐用云雾 AI(团队共用)</div>
|
||||
<p>找冰朔或肥猫获取团队 Key。填入后选「☁️ 云雾 AI(团队推荐)」提供商即可直接使用。</p>
|
||||
</div>
|
||||
<div class="role-guide">
|
||||
<div class="role-guide-title">没有 Key?用演示模式</div>
|
||||
|
|
@ -534,11 +567,13 @@ const ROLE_MAP = {
|
|||
};
|
||||
|
||||
const PROVS = {
|
||||
openai: {base:'https://api.openai.com/v1', mdls:['gpt-4o','gpt-4o-mini','gpt-4-turbo','gpt-3.5-turbo']},
|
||||
deepseek: {base:'https://api.deepseek.com/v1', mdls:['deepseek-chat','deepseek-reasoner']},
|
||||
moonshot: {base:'https://api.moonshot.cn/v1', mdls:['moonshot-v1-8k','moonshot-v1-32k','moonshot-v1-128k']},
|
||||
zhipu: {base:'https://open.bigmodel.cn/api/paas/v4', mdls:['glm-4','glm-4-flash','glm-4-air']},
|
||||
custom: {base:'', mdls:['gpt-4o']},
|
||||
yunwu: {base:'https://api.yunwu.ai/v1', mdls:['gpt-4o','gpt-4o-mini','claude-3-5-sonnet-20241022','deepseek-chat','gemini-1.5-pro']},
|
||||
openai: {base:'https://api.openai.com/v1', mdls:['gpt-4o','gpt-4o-mini','gpt-4-turbo','gpt-3.5-turbo']},
|
||||
gemini: {base:'https://generativelanguage.googleapis.com/v1beta/openai', mdls:['gemini-2.0-flash','gemini-2.0-flash-lite','gemini-1.5-pro','gemini-1.5-flash']},
|
||||
deepseek: {base:'https://api.deepseek.com/v1', mdls:['deepseek-chat','deepseek-reasoner']},
|
||||
moonshot: {base:'https://api.moonshot.cn/v1', mdls:['moonshot-v1-8k','moonshot-v1-32k','moonshot-v1-128k']},
|
||||
zhipu: {base:'https://open.bigmodel.cn/api/paas/v4', mdls:['glm-4','glm-4-flash','glm-4-air']},
|
||||
custom: {base:'', mdls:['gpt-4o']},
|
||||
};
|
||||
|
||||
const MODES = {
|
||||
|
|
@ -549,7 +584,11 @@ const MODES = {
|
|||
};
|
||||
const MODE_ORD = ['chat','build','review','brain'];
|
||||
|
||||
const DEFAULT_MDL = 'gpt-4o';
|
||||
const PROBE_TIMEOUT_MS = 8000; // ms per provider probe in autoDetectAPI
|
||||
const FB_COV = {implemented:3,total:17,percent:'17.6%'};
|
||||
// KEY_MASK is no longer used in the input field; kept only for backward compatibility
|
||||
// with any saved value that might still be in localStorage.
|
||||
const KEY_MASK = '(已保存)';
|
||||
|
||||
const FB_BRAIN = {
|
||||
|
|
@ -562,11 +601,17 @@ const FB_BRAIN = {
|
|||
// ═══════════════════════════════════════════════════════
|
||||
// STATE
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// Derive base URL from saved provider; never use stale zy_base for named providers
|
||||
const _initProv = ls('zy_prov')||'yunwu';
|
||||
const _initBase = _initProv === 'custom'
|
||||
? (ls('zy_base')||'')
|
||||
: (PROVS[_initProv]?.base||'https://api.yunwu.ai/v1');
|
||||
|
||||
const A = {
|
||||
key: ls('zy_key')||'',
|
||||
base: ls('zy_base')||'https://api.openai.com/v1',
|
||||
mdl: ls('zy_mdl')||'gpt-4o',
|
||||
prov: ls('zy_prov')||'openai',
|
||||
key: sessionStorage.getItem('zy_key')||'',
|
||||
base: _initBase,
|
||||
mdl: ls('zy_mdl')||DEFAULT_MDL,
|
||||
prov: _initProv,
|
||||
demo: false,
|
||||
brain: null,
|
||||
routing: null,
|
||||
|
|
@ -585,10 +630,26 @@ const A = {
|
|||
function ls(k){return localStorage.getItem(k)}
|
||||
function lss(k,v){localStorage.setItem(k,v)}
|
||||
|
||||
const RESET_KEYS = ['zy_key','zy_base','zy_mdl','zy_prov','zy_uname','zy_ghuser','zy_role'];
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// INIT
|
||||
// ═══════════════════════════════════════════════════════
|
||||
async function boot(){
|
||||
// URL-based reset: visiting the page with ?reset=1 clears all local settings.
|
||||
// This is a reliable escape hatch when the UI cannot be navigated.
|
||||
if(new URLSearchParams(location.search).get('reset')==='1'){
|
||||
RESET_KEYS.forEach(k => localStorage.removeItem(k));
|
||||
sessionStorage.removeItem('zy_key');
|
||||
A.key=''; A.base=PROVS['yunwu'].base; A.mdl=DEFAULT_MDL; A.prov='yunwu'; A.demo=false;
|
||||
A.userName=''; A.ghUser=''; A.role='guest';
|
||||
// Remove ?reset=1 from the URL so a subsequent refresh does not trigger another reset.
|
||||
history.replaceState(null,'',location.pathname);
|
||||
}
|
||||
// One-time migration: keys stored by old versions of this app in localStorage are
|
||||
// no longer used (keys now live only in sessionStorage). Clear any stale value
|
||||
// so it can never be accidentally picked up again.
|
||||
localStorage.removeItem('zy_key');
|
||||
initSetupUI();
|
||||
// Restore identity from localStorage
|
||||
if(A.userName) A.userMeta = ROLE_MAP[A.userName]||null;
|
||||
|
|
@ -615,24 +676,41 @@ function showApp(){
|
|||
// SETUP FORM
|
||||
// ═══════════════════════════════════════════════════════
|
||||
function initSetupUI(){
|
||||
const pv = A.prov||'openai';
|
||||
const pv = A.prov||'yunwu';
|
||||
document.getElementById('sp').value = pv;
|
||||
fillModels('sm', pv, A.mdl);
|
||||
if(pv==='custom'){
|
||||
const isCustom = pv==='custom';
|
||||
document.getElementById('sm').style.display = isCustom?'none':'block';
|
||||
document.getElementById('sm-cust').style.display = isCustom?'block':'none';
|
||||
document.getElementById('sm-cust-hint').style.display= isCustom?'block':'none';
|
||||
if(isCustom){
|
||||
document.getElementById('sm-cust').value = A.mdl||DEFAULT_MDL;
|
||||
document.getElementById('sep-g').style.display='block';
|
||||
document.getElementById('sep').value = A.base;
|
||||
} else {
|
||||
fillModels('sm', pv, A.mdl);
|
||||
}
|
||||
}
|
||||
|
||||
function onProv(pv,ctx){
|
||||
const mdlSel = ctx==='s'?'sm':'cm';
|
||||
const epGrp = ctx==='s'?'sep-g':'cep-g';
|
||||
const epInp = ctx==='s'?'sep':'cep';
|
||||
const cur = ctx==='s'? A.mdl : document.getElementById('cm')?.value;
|
||||
fillModels(mdlSel, pv, cur);
|
||||
const show = pv==='custom';
|
||||
document.getElementById(epGrp).style.display = show?'block':'none';
|
||||
if(!show){
|
||||
const mdlSel = ctx==='s'?'sm':'cm';
|
||||
const mdlCust = ctx==='s'?'sm-cust':'cm-cust';
|
||||
const mdlHint = ctx==='s'?'sm-cust-hint':'cm-cust-hint';
|
||||
const epGrp = ctx==='s'?'sep-g':'cep-g';
|
||||
const epInp = ctx==='s'?'sep':'cep';
|
||||
const cur = ctx==='s'? A.mdl : document.getElementById('cm')?.value;
|
||||
const isCustom = pv==='custom';
|
||||
// Toggle model input: dropdown for known providers, text input for custom
|
||||
document.getElementById(mdlSel).style.display = isCustom?'none':'block';
|
||||
document.getElementById(mdlCust).style.display = isCustom?'block':'none';
|
||||
document.getElementById(mdlHint).style.display = isCustom?'block':'none';
|
||||
if(isCustom){
|
||||
const custInp = document.getElementById(mdlCust);
|
||||
if(custInp && !custInp.value) custInp.value = cur||DEFAULT_MDL;
|
||||
} else {
|
||||
fillModels(mdlSel, pv, cur);
|
||||
}
|
||||
document.getElementById(epGrp).style.display = isCustom?'block':'none';
|
||||
if(!isCustom){
|
||||
const ep = document.getElementById(epInp);
|
||||
if(ep) ep.value = PROVS[pv]?.base||'';
|
||||
}
|
||||
|
|
@ -641,22 +719,110 @@ function onProv(pv,ctx){
|
|||
function fillModels(selId, pv, cur){
|
||||
const sel = document.getElementById(selId);
|
||||
if(!sel) return;
|
||||
const mdls = PROVS[pv]?.mdls||['gpt-4o'];
|
||||
const mdls = PROVS[pv]?.mdls||[DEFAULT_MDL];
|
||||
sel.innerHTML = mdls.map(m=>`<option value="${m}">${m}</option>`).join('');
|
||||
if(cur && mdls.includes(cur)) sel.value=cur;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// AUTO-DETECT PROVIDER & MODELS
|
||||
// ═══════════════════════════════════════════════════════
|
||||
async function autoDetectAPI(ctx){
|
||||
const keyEl = document.getElementById(ctx==='s'?'sk':'ck');
|
||||
const statusEl = document.getElementById(ctx==='s'?'sdet-status':'cdet-status');
|
||||
const btnEl = document.getElementById(ctx==='s'?'sdet-btn':'cdet-btn');
|
||||
const key = keyEl.value.trim();
|
||||
|
||||
if(!key){
|
||||
statusEl.innerHTML='<span style="color:var(--warn)">⚠️ 请先输入 API 密钥</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build probe list: custom endpoint first (if provided), then all named providers
|
||||
const candidates = [];
|
||||
const customBase = (document.getElementById(ctx==='s'?'sep':'cep')?.value||'').trim();
|
||||
if(customBase) candidates.push({pv:'custom', base:customBase});
|
||||
for(const [pv, cfg] of Object.entries(PROVS)){
|
||||
if(pv!=='custom' && cfg.base) candidates.push({pv, base:cfg.base});
|
||||
}
|
||||
|
||||
btnEl.disabled = true;
|
||||
btnEl.textContent = '⏳ 检测中…';
|
||||
statusEl.innerHTML = '<span style="color:var(--dim)">🔍 正在自动探测兼容提供商…</span>';
|
||||
|
||||
let matched = false;
|
||||
for(const {pv, base} of candidates){
|
||||
statusEl.innerHTML = `<span style="color:var(--dim)">🔍 探测 ${pv === 'custom' ? base : pv}…</span>`;
|
||||
try{
|
||||
const ctrl = new AbortController();
|
||||
const tid = setTimeout(()=>ctrl.abort(), PROBE_TIMEOUT_MS);
|
||||
const res = await fetch(base+'/models',{
|
||||
headers:{'Authorization':'Bearer '+key},
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
clearTimeout(tid);
|
||||
if(res.ok){
|
||||
// Expected: { data: [ { id: "model-name", ... }, ... ] } (OpenAI-compatible /models)
|
||||
const data = await res.json();
|
||||
const mdls = (data.data||[]).map(m=>m.id).filter(Boolean).sort();
|
||||
if(!mdls.length) continue;
|
||||
|
||||
// Switch provider select
|
||||
const provSel = document.getElementById(ctx==='s'?'sp':'cp');
|
||||
if(provSel) provSel.value = pv;
|
||||
onProv(pv, ctx);
|
||||
|
||||
// Override model dropdown with live list (always show as dropdown)
|
||||
const mdlSel = document.getElementById(ctx==='s'?'sm':'cm');
|
||||
const mdlCust = document.getElementById(ctx==='s'?'sm-cust':'cm-cust');
|
||||
const mdlHint = document.getElementById(ctx==='s'?'sm-cust-hint':'cm-cust-hint');
|
||||
if(mdlSel){
|
||||
mdlSel.innerHTML = mdls.map(m=>`<option value="${m}">${m}</option>`).join('');
|
||||
mdlSel.style.display = 'block';
|
||||
}
|
||||
if(mdlCust) mdlCust.style.display='none';
|
||||
if(mdlHint) mdlHint.style.display='none';
|
||||
|
||||
// If custom endpoint was the match, show the endpoint field
|
||||
if(pv==='custom' && ctx==='s'){
|
||||
document.getElementById('sep-g').style.display='block';
|
||||
document.getElementById('sep').value = customBase;
|
||||
} else if(pv==='custom' && ctx==='c'){
|
||||
document.getElementById('cep-g').style.display='block';
|
||||
document.getElementById('cep').value = customBase;
|
||||
}
|
||||
|
||||
const label = PROVS[pv]?.base ? pv : customBase;
|
||||
statusEl.innerHTML = `<span style="color:var(--ok)">✅ 检测成功(${label})· 共发现 <strong>${mdls.length}</strong> 个可用模型</span>`;
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}catch(e){ console.debug('autoDetect probe failed:', pv, e.message); }
|
||||
}
|
||||
|
||||
if(!matched){
|
||||
statusEl.innerHTML = '<span style="color:var(--err)">❌ 未能自动匹配,请手动选择提供商和模型后保存</span>';
|
||||
}
|
||||
|
||||
btnEl.disabled = false;
|
||||
btnEl.textContent = '🔍 自动检测';
|
||||
}
|
||||
|
||||
function doSetup(){
|
||||
const k = document.getElementById('sk').value.trim();
|
||||
const errEl = document.getElementById('sk-err');
|
||||
if(!k){errEl.style.display='block';return;}
|
||||
errEl.style.display='none';
|
||||
const pv = document.getElementById('sp').value;
|
||||
const md = document.getElementById('sm').value;
|
||||
const md = pv==='custom'
|
||||
? ((document.getElementById('sm-cust')?.value||'').trim()||DEFAULT_MDL)
|
||||
: document.getElementById('sm').value;
|
||||
let base = PROVS[pv]?.base||'';
|
||||
if(pv==='custom') base=(document.getElementById('sep')?.value||'').trim()||base;
|
||||
A.key=k; A.base=base; A.mdl=md; A.prov=pv; A.demo=false;
|
||||
lss('zy_key',k); lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov',pv);
|
||||
// Key is stored in sessionStorage only — it disappears when the tab/browser is closed.
|
||||
// This prevents stale keys from persisting across sessions.
|
||||
sessionStorage.setItem('zy_key', k); lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov',pv);
|
||||
// Save identity
|
||||
const un = document.getElementById('suid').value;
|
||||
const gh = (document.getElementById('sghuser')?.value||'').trim();
|
||||
|
|
@ -676,13 +842,27 @@ function doDemo(){
|
|||
// SETTINGS PANEL
|
||||
// ═══════════════════════════════════════════════════════
|
||||
function initSettingsPanel(){
|
||||
const pv = A.prov||'openai';
|
||||
const pv = A.prov||'yunwu';
|
||||
document.getElementById('cp').value = pv;
|
||||
fillModels('cm', pv, A.mdl);
|
||||
document.getElementById('ck').value = A.key?KEY_MASK:'';
|
||||
if(pv==='custom'){
|
||||
const isCustom = pv==='custom';
|
||||
document.getElementById('cm').style.display = isCustom?'none':'block';
|
||||
document.getElementById('cm-cust').style.display = isCustom?'block':'none';
|
||||
document.getElementById('cm-cust-hint').style.display= isCustom?'block':'none';
|
||||
if(isCustom){
|
||||
document.getElementById('cm-cust').value = A.mdl||DEFAULT_MDL;
|
||||
document.getElementById('cep-g').style.display='block';
|
||||
document.getElementById('cep').value = A.base;
|
||||
} else {
|
||||
fillModels('cm', pv, A.mdl);
|
||||
}
|
||||
// Always show EMPTY field — never prefill with a mask string.
|
||||
// Empty on save = keep old key; any typed value = replace key.
|
||||
document.getElementById('ck').value = '';
|
||||
const hint = document.getElementById('ck-hint');
|
||||
if(hint){
|
||||
hint.textContent = A.key
|
||||
? '🕐 密钥本次会话有效(末4位:…'+(A.key.length>=4?A.key.slice(-4):A.key)+')· 留空保持不变,输入新值即替换 · 关闭标签页自动清除'
|
||||
: '🔒 密钥仅本次会话有效,关闭标签页自动清除,请输入后保存';
|
||||
}
|
||||
if(A.userName) document.getElementById('cuid').value=A.userName;
|
||||
if(A.ghUser) document.getElementById('cghuser').value=A.ghUser;
|
||||
|
|
@ -690,13 +870,19 @@ function initSettingsPanel(){
|
|||
|
||||
function saveSet(){
|
||||
const pv = document.getElementById('cp').value;
|
||||
const md = document.getElementById('cm').value;
|
||||
const md = pv==='custom'
|
||||
? ((document.getElementById('cm-cust')?.value||'').trim()||A.mdl||DEFAULT_MDL)
|
||||
: document.getElementById('cm').value;
|
||||
const raw = document.getElementById('ck').value.trim();
|
||||
const k = raw===KEY_MASK? A.key : raw;
|
||||
// Empty field = keep existing key; any non-empty input = use as new key.
|
||||
// Never use KEY_MASK string comparison — that caused mixed-input bugs.
|
||||
const k = raw || A.key;
|
||||
let base = PROVS[pv]?.base||'';
|
||||
if(pv==='custom') base=(document.getElementById('cep').value.trim())||base;
|
||||
A.key=k; A.base=base; A.mdl=md; A.prov=pv; A.demo=!k;
|
||||
lss('zy_key',k); lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov',pv);
|
||||
// Key lives in sessionStorage only — cleared when tab/browser closes.
|
||||
if(k){ sessionStorage.setItem('zy_key', k); } else { sessionStorage.removeItem('zy_key'); }
|
||||
lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov',pv);
|
||||
// Save identity
|
||||
const un = document.getElementById('cuid')?.value||A.userName;
|
||||
const gh = document.getElementById('cghuser')?.value?.trim()||A.ghUser;
|
||||
|
|
@ -705,7 +891,18 @@ function saveSet(){
|
|||
document.getElementById('bbdemo').style.display=A.demo?'inline':'none';
|
||||
applyIdentityUI();
|
||||
closeP();
|
||||
botMsg('✅ 设置已保存。模型:<strong>'+esc(md)+'</strong>'+(un?' · 身份:'+esc(un):'')+(A.demo?'<br>⚠️ 未设置 API 密钥,演示模式仍然开启。':''));
|
||||
const keyNote = raw ? ' · 🔑 新密钥已保存(末4位:…'+(k.length>=4?k.slice(-4):k)+')' : ' · 🔑 密钥未变更';
|
||||
botMsg('✅ 设置已保存。模型:<strong>'+esc(md)+'</strong>'+keyNote+(un?' · 身份:'+esc(un):'')+(A.demo?'<br>⚠️ 未设置 API 密钥,演示模式仍然开启。':''));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// RESET ALL SETTINGS
|
||||
// ═══════════════════════════════════════════════════════
|
||||
function resetAllSettings(){
|
||||
if(!confirm('确定要清除所有本地设置吗?\n\n· API 密钥会被删除\n· 身份设置会被删除\n· 将返回初始设置界面\n\n这不会影响任何聊天记录或云端数据。')) return;
|
||||
RESET_KEYS.forEach(k => localStorage.removeItem(k));
|
||||
sessionStorage.removeItem('zy_key');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
|
@ -913,10 +1110,13 @@ async function streamReply(txt){
|
|||
}),
|
||||
});
|
||||
if(!res.ok){
|
||||
const provName=PROVS[A.prov]?.base||A.base;
|
||||
let em=provName+' 返回错误 '+res.status;
|
||||
const provLabel = A.prov==='custom' ? A.base : (A.prov+'('+A.base+')');
|
||||
let em='['+provLabel+'] 返回错误 '+res.status;
|
||||
let isAuthErr = (res.status===401||res.status===403);
|
||||
try{const e=await res.json();em=e.error?.message||em;}catch(_){}
|
||||
throw new Error(em);
|
||||
const err = new Error(em);
|
||||
err.isAuth = isAuthErr;
|
||||
throw err;
|
||||
}
|
||||
let full='';
|
||||
const rdr=res.body.getReader();
|
||||
|
|
@ -942,7 +1142,17 @@ async function streamReply(txt){
|
|||
A.msgs.push({role:'assistant',content:full});
|
||||
addCopyBtns(el);
|
||||
}catch(e){
|
||||
el.innerHTML='<span class="err">⚠️ '+esc(e.message)+'</span><br><small style="color:var(--dim)">请检查 API 密钥和网络连接,或在 ⚙️ 设置中更换模型。</small>';
|
||||
if(e.isAuth){
|
||||
el.innerHTML='<span class="err">🔑 API 密钥认证失败</span>'
|
||||
+'<br><small style="color:var(--dim)">'+esc(e.message)+'</small>'
|
||||
+'<br><div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">'
|
||||
+'<button onclick="openPanel(\'sp2\')" style="padding:6px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;cursor:pointer;font-size:13px">⚙️ 更新 API 密钥</button>'
|
||||
+'<button onclick="resetAllSettings()" style="padding:6px 14px;border-radius:6px;border:1px solid #ef4444;background:transparent;color:#ef4444;cursor:pointer;font-size:13px">🔄 清除重置</button>'
|
||||
+'</div>'
|
||||
+'<small style="color:var(--dim);display:block;margin-top:6px">打开设置 → 直接输入新密钥 → 保存(无需清空,留空 = 保留旧密钥)</small>';
|
||||
} else {
|
||||
el.innerHTML='<span class="err">⚠️ '+esc(e.message)+'</span><br><small style="color:var(--dim)">请检查 API 密钥和网络连接,或在 ⚙️ 设置中更换模型。</small>';
|
||||
}
|
||||
A.msgs.pop();
|
||||
}finally{
|
||||
A.streaming=false; updSendBtn(); scrollB();
|
||||
|
|
@ -1170,7 +1380,9 @@ function closeP(){
|
|||
// ═══════════════════════════════════════════════════════
|
||||
// COPY URL (share link)
|
||||
// ═══════════════════════════════════════════════════════
|
||||
const CHAT_URL = 'https://' + REPO.split('/')[0] + '.github.io/' + REPO.split('/')[1] + '/';
|
||||
// Auto-detect the real URL so the share link is always correct
|
||||
// regardless of whether Pages serves from docs/ or repo root.
|
||||
const CHAT_URL = window.location.origin + window.location.pathname;
|
||||
|
||||
function showCopyFeedback(success){
|
||||
const btn=document.querySelector('.copy-url-btn');
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"name": "nextjs",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"next": "15.3.2",
|
||||
"next": "15.3.8",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
|
|
@ -1511,15 +1511,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.2.tgz",
|
||||
"integrity": "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g==",
|
||||
"version": "15.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.8.tgz",
|
||||
"integrity": "sha512-SAfHg0g91MQVMPioeFeDjE+8UPF3j3BvHjs8ZKJAUz1BG7eMPvfCKOAgNWJ6s1MLNeP6O2InKQRTNblxPWuq+Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.2.tgz",
|
||||
"integrity": "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.5.tgz",
|
||||
"integrity": "sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1533,9 +1533,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.2.tgz",
|
||||
"integrity": "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.5.tgz",
|
||||
"integrity": "sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1549,9 +1549,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.2.tgz",
|
||||
"integrity": "sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.5.tgz",
|
||||
"integrity": "sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1565,9 +1565,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.2.tgz",
|
||||
"integrity": "sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.5.tgz",
|
||||
"integrity": "sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1581,9 +1581,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz",
|
||||
"integrity": "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.5.tgz",
|
||||
"integrity": "sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1597,9 +1597,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz",
|
||||
"integrity": "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.5.tgz",
|
||||
"integrity": "sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1613,9 +1613,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.2.tgz",
|
||||
"integrity": "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.5.tgz",
|
||||
"integrity": "sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1629,9 +1629,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.2.tgz",
|
||||
"integrity": "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.5.tgz",
|
||||
"integrity": "sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1917,6 +1917,66 @@
|
|||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.4.3",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.0.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.3",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.0.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.9",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.0",
|
||||
"@emnapi/runtime": "^1.4.0",
|
||||
"@tybys/wasm-util": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.9.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||
"version": "2.8.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.5.tgz",
|
||||
|
|
@ -4575,12 +4635,12 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.3.2",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.3.2.tgz",
|
||||
"integrity": "sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ==",
|
||||
"version": "15.3.8",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.3.8.tgz",
|
||||
"integrity": "sha512-L+4c5Hlr84fuaNADZbB9+ceRX9/CzwxJ+obXIGHupboB/Q1OLbSUapFs4bO8hnS/E6zV/JDX7sG1QpKVR2bguA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.3.2",
|
||||
"@next/env": "15.3.8",
|
||||
"@swc/counter": "0.1.3",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"busboy": "1.6.0",
|
||||
|
|
@ -4595,14 +4655,14 @@
|
|||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.3.2",
|
||||
"@next/swc-darwin-x64": "15.3.2",
|
||||
"@next/swc-linux-arm64-gnu": "15.3.2",
|
||||
"@next/swc-linux-arm64-musl": "15.3.2",
|
||||
"@next/swc-linux-x64-gnu": "15.3.2",
|
||||
"@next/swc-linux-x64-musl": "15.3.2",
|
||||
"@next/swc-win32-arm64-msvc": "15.3.2",
|
||||
"@next/swc-win32-x64-msvc": "15.3.2",
|
||||
"@next/swc-darwin-arm64": "15.3.5",
|
||||
"@next/swc-darwin-x64": "15.3.5",
|
||||
"@next/swc-linux-arm64-gnu": "15.3.5",
|
||||
"@next/swc-linux-arm64-musl": "15.3.5",
|
||||
"@next/swc-linux-x64-gnu": "15.3.5",
|
||||
"@next/swc-linux-x64-musl": "15.3.5",
|
||||
"@next/swc-win32-arm64-msvc": "15.3.5",
|
||||
"@next/swc-win32-x64-msvc": "15.3.5",
|
||||
"sharp": "^0.34.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
"broadcast:distribute": "node scripts/distribute-broadcasts.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "15.3.2",
|
||||
"next": "15.3.8",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,366 @@
|
|||
// scripts/notion-bridge.js
|
||||
// 铸渊 → Notion 数据桥(共用模块)
|
||||
//
|
||||
// 管道 A (syslog): node scripts/notion-bridge.js syslog
|
||||
// 管道 E (changes): node scripts/notion-bridge.js changes
|
||||
//
|
||||
// 必需环境变量:
|
||||
// NOTION_TOKEN GitHub Secret: NOTION_TOKEN
|
||||
//
|
||||
// 可选环境变量(有内置默认值):
|
||||
// SYSLOG_DB_ID 「GitHub SYSLOG 收件箱」database_id(默认已内置)
|
||||
// CHANGES_DB_ID 「GitHub 变更日志」database_id(默认已内置)
|
||||
//
|
||||
// 管道 E 额外环境变量(由 workflow 注入,见下方说明)
|
||||
|
||||
'use strict';
|
||||
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// 常量
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
const NOTION_VERSION = '2022-06-28';
|
||||
const NOTION_API_HOSTNAME = 'api.notion.com';
|
||||
const NOTION_RICH_TEXT_MAX = 2000; // Notion rich_text 单个 text object 内容上限
|
||||
const NOTION_TITLE_MAX = 120; // Notion 标题属性建议截断长度
|
||||
const UNKNOWN_COMMITTER = '未知'; // 提交者信息缺失时的默认值
|
||||
|
||||
// Notion 数据库 ID(霜砚已在 Notion 侧建好,ID 固定)
|
||||
const DEFAULT_SYSLOG_DB_ID = '330ab17507d542c9bbb96d0749b41197';
|
||||
const DEFAULT_CHANGES_DB_ID = 'e740b77aa6bd4ac0a2e8a75f678fba98';
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// Notion API 基础调用
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 向 Notion API 发起 HTTPS POST 请求
|
||||
* @param {string} endpoint - e.g. '/v1/pages'
|
||||
* @param {object} body - JSON body
|
||||
* @param {string} token - Bearer token
|
||||
* @returns {Promise<object>} - Notion API 返回值
|
||||
*/
|
||||
function notionPost(endpoint, body, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = JSON.stringify(body);
|
||||
const opts = {
|
||||
hostname: NOTION_API_HOSTNAME,
|
||||
port: 443,
|
||||
path: endpoint,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json',
|
||||
'Notion-Version': NOTION_VERSION,
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
},
|
||||
};
|
||||
|
||||
const req = https.request(opts, res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(parsed);
|
||||
} else {
|
||||
reject(new Error(`Notion API ${res.statusCode}: ${parsed.message || data}`));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error(`Notion API parse error: ${data}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// Notion 属性构建辅助函数
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 构建 Notion rich_text 属性值
|
||||
* 自动将超过 NOTION_RICH_TEXT_MAX 的内容切分为多个 text object
|
||||
* @param {string} content
|
||||
* @returns {Array}
|
||||
*/
|
||||
function richText(content) {
|
||||
const str = String(content || '');
|
||||
const chunks = [];
|
||||
for (let i = 0; i < str.length; i += NOTION_RICH_TEXT_MAX) {
|
||||
chunks.push({ type: 'text', text: { content: str.slice(i, i + NOTION_RICH_TEXT_MAX) } });
|
||||
}
|
||||
return chunks.length ? chunks : [{ type: 'text', text: { content: '' } }];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Notion title 属性值
|
||||
* @param {string} content
|
||||
*/
|
||||
function titleProp(content) {
|
||||
return [{ type: 'text', text: { content: String(content || '').slice(0, NOTION_TITLE_MAX) } }];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Notion paragraph block(用于页面正文)
|
||||
* @param {string} content
|
||||
*/
|
||||
function paragraph(content) {
|
||||
return {
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
paragraph: { rich_text: richText(String(content || '').slice(0, NOTION_RICH_TEXT_MAX)) },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Notion heading_3 block
|
||||
*/
|
||||
function heading3(content) {
|
||||
return {
|
||||
object: 'block',
|
||||
type: 'heading_3',
|
||||
heading_3: { rich_text: [{ type: 'text', text: { content: String(content || '') } }] },
|
||||
};
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// 管道 A · SYSLOG 收件箱同步到 Notion
|
||||
// ══════════════════════════════════════════════════════════
|
||||
//
|
||||
// Notion「📥 GitHub SYSLOG 收件箱」数据库属性(霜砚已建):
|
||||
// 标题 title SYSLOG 文件名
|
||||
// DEV编号 select 开发者编号,如 DEV-001
|
||||
// 文件内容 rich_text SYSLOG 文件完整文本
|
||||
// 接收时间 date 推送时间
|
||||
// 处理状态 status 固定填「待处理」
|
||||
// 来源路径 rich_text GitHub 中的文件路径
|
||||
// commit_sha rich_text 对应的 Git commit SHA
|
||||
// 推送方 rich_text 固定填「铸渊」
|
||||
|
||||
/**
|
||||
* 将单条 syslog entry 写入 Notion 数据库
|
||||
* @param {string} dbId - 「GitHub SYSLOG 收件箱」database_id
|
||||
* @param {string} fileContent - 文件原始内容(字符串)
|
||||
* @param {string} filePath - 相对路径(用于溯源)
|
||||
* @param {object} entry - 解析后的 JSON(或空对象)
|
||||
* @param {string} commitSha - Git commit SHA
|
||||
* @param {string} token - Notion token
|
||||
*/
|
||||
async function createSyslogRecord(dbId, fileContent, filePath, entry, commitSha, token) {
|
||||
const filename = path.basename(filePath);
|
||||
const title = entry.title || filename;
|
||||
const devId = entry.from || entry.dev_id || '';
|
||||
const ts = entry.timestamp || new Date().toISOString();
|
||||
|
||||
const properties = {
|
||||
'标题': { title: titleProp(title) },
|
||||
'文件内容': { rich_text: richText(fileContent) },
|
||||
'来源路径': { rich_text: richText(filePath) },
|
||||
'接收时间': { date: { start: ts } },
|
||||
'处理状态': { status: { name: '待处理' } },
|
||||
'commit_sha': { rich_text: richText(commitSha || '') },
|
||||
'推送方': { rich_text: richText('铸渊') },
|
||||
};
|
||||
|
||||
// DEV编号 select 只在有值时设置(空值会导致 Notion API 报错)
|
||||
if (devId) {
|
||||
properties['DEV编号'] = { select: { name: devId } };
|
||||
}
|
||||
|
||||
const body = {
|
||||
parent: { database_id: dbId },
|
||||
properties,
|
||||
};
|
||||
|
||||
return notionPost('/v1/pages', body, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归扫描目录,返回所有匹配的文件路径
|
||||
*/
|
||||
function scanDir(dir, extensions) {
|
||||
const results = [];
|
||||
if (!fs.existsSync(dir)) return results;
|
||||
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.name === '.gitkeep') continue;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...scanDir(fullPath, extensions));
|
||||
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async function runPipelineA() {
|
||||
const token = process.env.NOTION_TOKEN;
|
||||
const dbId = process.env.SYSLOG_DB_ID || DEFAULT_SYSLOG_DB_ID;
|
||||
const commitSha = process.env.COMMIT_SHA || '';
|
||||
|
||||
if (!token) {
|
||||
console.log('⚠️ 管道A: 缺少 NOTION_TOKEN,跳过 Notion 同步');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const INBOX_DIR = 'syslog-inbox';
|
||||
const files = scanDir(INBOX_DIR, ['.json', '.md', '.txt']);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('📭 syslog-inbox/ 无待处理条目,跳过 Notion 同步');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`📥 管道A: 发现 ${files.length} 条 syslog,开始同步到 Notion…`);
|
||||
|
||||
let ok = 0, failed = 0;
|
||||
|
||||
for (const fullPath of files) {
|
||||
const relPath = fullPath.replace(/\\/g, '/'); // normalize on Windows
|
||||
let raw, entry;
|
||||
|
||||
try {
|
||||
raw = fs.readFileSync(fullPath, 'utf8');
|
||||
entry = fullPath.endsWith('.json') ? JSON.parse(raw) : {};
|
||||
} catch (e) {
|
||||
console.error(`❌ 读取 ${relPath} 失败: ${e.message}`);
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const page = await createSyslogRecord(dbId, raw, relPath, entry, commitSha, token);
|
||||
console.log(` ✅ ${relPath} → Notion page: ${page.id}`);
|
||||
ok++;
|
||||
} catch (e) {
|
||||
console.error(` ❌ ${relPath} → Notion 失败: ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ 管道A Notion 同步完成 · 成功 ${ok} 条 · 失败 ${failed} 条`);
|
||||
if (failed > 0) process.exit(1);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// 管道 E · GitHub 变更日志同步到 Notion
|
||||
// ══════════════════════════════════════════════════════════
|
||||
//
|
||||
// Notion「📋 GitHub 变更日志」数据库属性(霜砚已建):
|
||||
// 标题 title commit message 或 PR标题
|
||||
// 提交者 select DEV编号或"铸渊""妈妈"
|
||||
// 变更类型 select Commit / PR opened / PR merged / PR closed
|
||||
// 变更文件 rich_text 变更的文件路径列表
|
||||
// 提交时间 date Git提交时间
|
||||
// commit_sha rich_text Git commit SHA
|
||||
// PR编号 rich_text PR号(PR事件)
|
||||
// 分支 rich_text 分支名
|
||||
// 霜砚已读 checkbox 固定填 false
|
||||
//
|
||||
// 环境变量(由 workflow 注入):
|
||||
// EVENT_TYPE commit | pr
|
||||
// COMMIT_SHA 提交 SHA
|
||||
// COMMIT_MSG commit message(push 事件)
|
||||
// COMMITTER 提交者用户名
|
||||
// COMMIT_TIMESTAMP 提交时间 ISO
|
||||
// CHANGED_FILES 换行符分隔的变更文件路径列表
|
||||
// BRANCH 分支名
|
||||
// PR_NUMBER PR 编号(pr 事件)
|
||||
// PR_TITLE PR 标题(pr 事件)
|
||||
// PR_ACTION opened | closed(pr 事件)
|
||||
// PR_MERGED true | false(pr 事件)
|
||||
|
||||
async function runPipelineE() {
|
||||
const token = process.env.NOTION_TOKEN;
|
||||
const dbId = process.env.CHANGES_DB_ID || DEFAULT_CHANGES_DB_ID;
|
||||
|
||||
if (!token) {
|
||||
console.log('⚠️ 管道E: 缺少 NOTION_TOKEN,跳过 Notion 同步');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const eventType = process.env.EVENT_TYPE || 'commit';
|
||||
const now = process.env.COMMIT_TIMESTAMP || new Date().toISOString();
|
||||
const commitSha = process.env.COMMIT_SHA || '';
|
||||
const branch = process.env.BRANCH || '';
|
||||
const changedFiles = (process.env.CHANGED_FILES || '').trim();
|
||||
const committer = process.env.COMMITTER || UNKNOWN_COMMITTER;
|
||||
|
||||
let title, changeType, prNumber;
|
||||
|
||||
if (eventType === 'pr') {
|
||||
const action = process.env.PR_ACTION || 'opened';
|
||||
const merged = process.env.PR_MERGED === 'true';
|
||||
prNumber = process.env.PR_NUMBER || '';
|
||||
const prTitle = process.env.PR_TITLE || '(无标题)';
|
||||
|
||||
if (merged) changeType = 'PR merged';
|
||||
else if (action === 'closed') changeType = 'PR closed';
|
||||
else changeType = 'PR opened';
|
||||
|
||||
title = `PR #${prNumber}: ${prTitle}`.slice(0, NOTION_TITLE_MAX);
|
||||
} else {
|
||||
const msg = process.env.COMMIT_MSG || '(无 commit message)';
|
||||
title = msg.split('\n')[0].slice(0, NOTION_TITLE_MAX);
|
||||
changeType = 'Commit';
|
||||
prNumber = '';
|
||||
}
|
||||
|
||||
console.log(`📡 管道E: 同步变更记录到 Notion: ${title}`);
|
||||
|
||||
const properties = {
|
||||
'标题': { title: titleProp(title) },
|
||||
'变更类型': { select: { name: changeType } },
|
||||
'变更文件': { rich_text: richText(changedFiles) },
|
||||
'提交时间': { date: { start: now } },
|
||||
'commit_sha': { rich_text: richText(commitSha) },
|
||||
'PR编号': { rich_text: richText(prNumber || '') },
|
||||
'分支': { rich_text: richText(branch) },
|
||||
'霜砚已读': { checkbox: false },
|
||||
};
|
||||
|
||||
// 提交者 select 只在有值时设置
|
||||
if (committer && committer !== UNKNOWN_COMMITTER) {
|
||||
properties['提交者'] = { select: { name: committer } };
|
||||
}
|
||||
|
||||
const body = {
|
||||
parent: { database_id: dbId },
|
||||
properties,
|
||||
};
|
||||
|
||||
try {
|
||||
const page = await notionPost('/v1/pages', body, token);
|
||||
console.log(`✅ 管道E 变更记录已写入 Notion: ${page.id}`);
|
||||
} catch (e) {
|
||||
console.error(`❌ 管道E 写入 Notion 失败: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// 入口
|
||||
// ══════════════════════════════════════════════════════════
|
||||
|
||||
const mode = process.argv[2];
|
||||
|
||||
if (mode === 'syslog') {
|
||||
runPipelineA().catch(e => { console.error(e); process.exit(1); });
|
||||
} else if (mode === 'changes') {
|
||||
runPipelineE().catch(e => { console.error(e); process.exit(1); });
|
||||
} else {
|
||||
console.error('用法: node scripts/notion-bridge.js [syslog|changes]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
// scripts/process-syslog.js
|
||||
// 铸渊 SYSLOG Pipeline
|
||||
// 读取 syslog-inbox/ 下的日志条目,处理后归档到 syslog-processed/
|
||||
// 触发:syslog-inbox/ 目录有新文件 push 到 main 分支
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const INBOX_DIR = 'syslog-inbox';
|
||||
const ARCHIVE_DIR = 'syslog-processed';
|
||||
const BRAIN_DIR = '.github/brain';
|
||||
const MEMORY_PATH = path.join(BRAIN_DIR, 'memory.json');
|
||||
const DEV_STATUS_PATH = '.github/persona-brain/dev-status.json';
|
||||
|
||||
// ─── 加载大脑 ────────────────────────────────────────────
|
||||
let memory = {};
|
||||
try {
|
||||
memory = JSON.parse(fs.readFileSync(MEMORY_PATH, 'utf8'));
|
||||
} catch (_) {
|
||||
memory = { events: [], stats: { broadcasts_processed: 0 } };
|
||||
}
|
||||
if (!memory.events) memory.events = [];
|
||||
if (!memory.stats) memory.stats = {};
|
||||
if (!memory.stats.broadcasts_processed) memory.stats.broadcasts_processed = 0;
|
||||
if (!memory.stats.syslog_processed) memory.stats.syslog_processed = 0;
|
||||
|
||||
// ─── 扫描 inbox ──────────────────────────────────────────
|
||||
if (!fs.existsSync(INBOX_DIR)) {
|
||||
console.log('📭 syslog-inbox/ 目录不存在,跳过');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(INBOX_DIR)
|
||||
.filter(f => f.endsWith('.json') && f !== '.gitkeep');
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('📭 syslog-inbox/ 无待处理条目');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`📥 发现 ${files.length} 条 syslog 条目,开始处理...\n`);
|
||||
|
||||
// ─── 按月归档路径 ─────────────────────────────────────────
|
||||
function archiveDir(timestamp) {
|
||||
const month = (timestamp || new Date().toISOString()).slice(0, 7); // "2026-03"
|
||||
const dir = path.join(ARCHIVE_DIR, month);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
// ─── 处理每条 syslog ──────────────────────────────────────
|
||||
files.forEach(file => {
|
||||
const fullPath = path.join(INBOX_DIR, file);
|
||||
let entry;
|
||||
|
||||
try {
|
||||
entry = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
||||
} catch (e) {
|
||||
console.error(`❌ [INVALID JSON] ${file}: ${e.message},跳过`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📋 处理: ${file}`);
|
||||
console.log(` 类型: ${entry.type || '未知'} · 来自: ${entry.from || '未知'} · 标题: ${entry.title || ''}`);
|
||||
|
||||
const event = {
|
||||
timestamp: entry.timestamp || new Date().toISOString(),
|
||||
type: 'syslog_' + (entry.type || 'unknown'),
|
||||
syslog_id: entry.syslog_id || file,
|
||||
title: entry.title || '(无标题)',
|
||||
from: entry.from || '未知',
|
||||
file,
|
||||
};
|
||||
|
||||
// ── 根据类型分发处理 ──
|
||||
switch (entry.type) {
|
||||
|
||||
case 'broadcast': {
|
||||
// 广播类:追加到 brain/memory.json events,与现有 broadcast pipeline 互补
|
||||
memory.stats.broadcasts_processed += 1;
|
||||
event.content_preview = (entry.content || '').slice(0, 80);
|
||||
console.log(' ✅ 广播已记录到大脑记忆');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'auth': {
|
||||
// 授权类:记录授权事件,写入 memory
|
||||
event.target_dev_id = entry.target_dev_id;
|
||||
event.target_name = entry.target_name;
|
||||
event.permission_level = entry.permission_level;
|
||||
event.authorized_by = entry.authorized_by || '冰朔';
|
||||
event.valid_until = entry.valid_until;
|
||||
console.log(` ✅ 授权记录:${entry.target_name}(${entry.target_dev_id})· 权限:${entry.permission_level}`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'inspect': {
|
||||
// 巡检类:记录巡检结果
|
||||
event.inspect_result = entry.result || 'unknown';
|
||||
event.issues_found = entry.issues_found || 0;
|
||||
console.log(` ✅ 巡检记录:结果 ${event.inspect_result} · 发现问题 ${event.issues_found} 个`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'alert': {
|
||||
// 告警类
|
||||
event.severity = entry.priority || 'normal';
|
||||
console.log(` ⚠️ 告警记录:${entry.title} · 优先级 ${event.severity}`);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(` ℹ️ 未知类型 "${entry.type}",已记录原始内容`);
|
||||
event.raw = entry;
|
||||
}
|
||||
|
||||
// 写入大脑记忆
|
||||
memory.events.push(event);
|
||||
memory.stats.syslog_processed = (memory.stats.syslog_processed || 0) + 1;
|
||||
memory.last_updated = new Date().toISOString();
|
||||
|
||||
// 归档文件
|
||||
const destDir = archiveDir(entry.timestamp);
|
||||
const destPath = path.join(destDir, file);
|
||||
fs.renameSync(fullPath, destPath);
|
||||
console.log(` 📦 已归档到 ${destPath}\n`);
|
||||
});
|
||||
|
||||
// ─── 保存大脑 ─────────────────────────────────────────────
|
||||
// 只保留最近 100 条事件,防止 memory.json 无限膨胀
|
||||
if (memory.events.length > 100) {
|
||||
memory.events = memory.events.slice(-100);
|
||||
}
|
||||
fs.writeFileSync(MEMORY_PATH, JSON.stringify(memory, null, 2));
|
||||
|
||||
console.log(`✅ SYSLOG Pipeline 完成 · 累计处理 ${memory.stats.syslog_processed} 条`);
|
||||
|
|
@ -32,6 +32,25 @@ if (missing.length > 0) {
|
|||
console.log('✅ 大脑文件完整性:全部就绪');
|
||||
}
|
||||
|
||||
// === ① bis · 目录结构巡检 ===
|
||||
const requiredDirs = ['syslog-inbox', 'syslog-processed', 'broadcasts-outbox', '.github/brain'];
|
||||
const missingDirs = requiredDirs.filter(d => !fs.existsSync(d));
|
||||
if (missingDirs.length > 0) {
|
||||
console.error('⚠️ 缺失目录:' + missingDirs.join(', '));
|
||||
} else {
|
||||
console.log('✅ 目录结构巡检:syslog-inbox / syslog-processed / broadcasts-outbox 全部就绪');
|
||||
}
|
||||
|
||||
// 统计 syslog-inbox 待处理条目
|
||||
const inboxFiles = fs.existsSync('syslog-inbox')
|
||||
? fs.readdirSync('syslog-inbox').filter(f => f.endsWith('.json')).length
|
||||
: 0;
|
||||
if (inboxFiles > 0) {
|
||||
console.log(`📥 syslog-inbox 待处理条目:${inboxFiles} 个(建议触发 syslog-pipeline workflow)`);
|
||||
} else {
|
||||
console.log('📭 syslog-inbox 无待处理条目');
|
||||
}
|
||||
|
||||
// === ② 知识库去重与整理 ===
|
||||
const seen = new Set();
|
||||
const uniqueFaq = kb.faq.filter(item => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
# syslog-inbox · 系统日志收件箱
|
||||
|
||||
此目录由 **Notion 侧霜砚人格体** 写入,GitHub 侧铸渊 Pipeline 读取处理。
|
||||
|
||||
## 目录用途
|
||||
|
||||
| 角色 | 操作 |
|
||||
|------|------|
|
||||
| Notion 侧(霜砚) | 将广播/授权/巡检日志写入此目录 |
|
||||
| GitHub 侧(铸渊) | Pipeline 检测到新文件,处理后移入 `syslog-processed/` |
|
||||
|
||||
## 文件命名规范
|
||||
|
||||
```
|
||||
{类型}-{日期}-{编号}.json
|
||||
```
|
||||
|
||||
示例:
|
||||
- `broadcast-2026-03-06-001.json` — 霜砚广播
|
||||
- `auth-2026-03-06-DEV002.json` — 用户授权回执
|
||||
- `inspect-2026-03-06-001.json` — 巡检记录
|
||||
|
||||
## 标准 JSON 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"syslog_id": "BC-2026-03-06-001",
|
||||
"type": "broadcast | auth | inspect | alert",
|
||||
"from": "霜砚",
|
||||
"to": "铸渊",
|
||||
"timestamp": "2026-03-06T08:00:00Z",
|
||||
"title": "广播标题",
|
||||
"content": "内容正文",
|
||||
"target_dev_id": "DEV-002",
|
||||
"priority": "normal | high | urgent"
|
||||
}
|
||||
```
|
||||
|
||||
## 授权广播格式(auth 类型)
|
||||
|
||||
冰朔通过 Notion 侧霜砚下发授权时,使用此格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"syslog_id": "AUTH-2026-03-06-DEV002",
|
||||
"type": "auth",
|
||||
"from": "霜砚",
|
||||
"authorized_by": "冰朔",
|
||||
"timestamp": "2026-03-06T08:00:00Z",
|
||||
"title": "用户授权",
|
||||
"target_dev_id": "DEV-002",
|
||||
"target_name": "肥猫",
|
||||
"permission_level": "supreme",
|
||||
"valid_until": "2026-12-31T23:59:59Z"
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ 写入后请勿手动修改或删除,Pipeline 处理完成后会自动归档到 `syslog-processed/`。
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# syslog-processed · 已处理日志归档
|
||||
|
||||
此目录由 **铸渊 Pipeline** 自动管理,存放已处理完成的 syslog 条目。
|
||||
|
||||
- 文件从 `syslog-inbox/` 处理完毕后自动移入此处
|
||||
- 按月分子目录归档:`syslog-processed/2026-03/`
|
||||
- **请勿手动修改此目录内容**
|
||||
|
||||
> 此目录仅作归档用途,任何查询请读取 `.github/brain/memory.json`。
|
||||
Loading…
Reference in New Issue