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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches:
|
||||||
|
- main
|
||||||
|
- copilot/** # Copilot Agent 功能分支可直接部署预览,PR 合并后自动失效
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- '.github/brain/**'
|
- '.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. **选身份** → 下拉菜单选「❄️ 冰朔(语言架构师·创始人)」
|
2. **选身份** → 下拉菜单选「❄️ 冰朔(语言架构师·创始人)」
|
||||||
3. **填 API 密钥** → 输入你的云雾/OpenAI/DeepSeek API Key
|
3. **填 API 密钥** → 输入你的云雾/OpenAI/DeepSeek API Key
|
||||||
4. **点「开始对话」** → 铸渊自动唤醒,汇报大脑状态
|
4. **点「开始对话」** → 铸渊自动唤醒,汇报大脑状态
|
||||||
|
|
@ -30,7 +30,7 @@ https://qinfendebingshuo.github.io/guanghulab/
|
||||||
|
|
||||||
### 🦁 肥猫(光湖团队总控 · DEV-002)
|
### 🦁 肥猫(光湖团队总控 · DEV-002)
|
||||||
|
|
||||||
1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/
|
1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/docs/index.html
|
||||||
2. **选身份** → 下拉菜单选「🦁 肥猫(光湖团队总控·DEV-002)」
|
2. **选身份** → 下拉菜单选「🦁 肥猫(光湖团队总控·DEV-002)」
|
||||||
3. **填 API 密钥** → 输入自己的 API Key(或点「演示模式」也能看进度面板)
|
3. **填 API 密钥** → 输入自己的 API Key(或点「演示模式」也能看进度面板)
|
||||||
4. **点「开始对话」** → 铸渊自动打开**总控指挥台**,显示全员进度
|
4. **点「开始对话」** → 铸渊自动打开**总控指挥台**,显示全员进度
|
||||||
|
|
@ -44,7 +44,7 @@ https://qinfendebingshuo.github.io/guanghulab/
|
||||||
|
|
||||||
### 🍊 桔子(光湖主控 · DEV-010)
|
### 🍊 桔子(光湖主控 · DEV-010)
|
||||||
|
|
||||||
1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/
|
1. **打开链接** → https://qinfendebingshuo.github.io/guanghulab/docs/index.html
|
||||||
2. **选身份** → 下拉菜单选「🍊 桔子(光湖主控·DEV-010)」
|
2. **选身份** → 下拉菜单选「🍊 桔子(光湖主控·DEV-010)」
|
||||||
3. **填 API 密钥** → 输入自己的 API Key
|
3. **填 API 密钥** → 输入自己的 API Key
|
||||||
4. **点「开始对话」** → 铸渊显示你当前模块进度,并开放指挥台
|
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. **选身份** → 下拉菜单选自己的名字
|
2. **选身份** → 下拉菜单选自己的名字
|
||||||
3. **填 API 密钥** → 填自己的 Key(没有的话点「演示模式」)
|
3. **填 API 密钥** → 填自己的 Key(没有的话点「演示模式」)
|
||||||
4. **点「开始对话」** → 铸渊显示你的当前任务状态,可以问问题
|
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 就能和铸渊说话了。
|
打开后选择你的身份,填入 API Key 就能和铸渊说话了。
|
||||||
```
|
```
|
||||||
|
|
|
||||||
300
docs/index.html
300
docs/index.html
|
|
@ -1,8 +1,12 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
<!-- 铸渊助手 v3.1 · 2026-03-06 · auto-detect API + Gemini + sessionStorage key -->
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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>
|
<title>铸渊助手 · HoloLake AI</title>
|
||||||
<style>
|
<style>
|
||||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
*,*::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}
|
.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{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}
|
.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}
|
.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}
|
.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}
|
.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>
|
<div class="sc-logo">🌀</div>
|
||||||
<h1>铸渊助手</h1>
|
<h1>铸渊助手</h1>
|
||||||
<p class="sub">HoloLake · 代码守护人格体 · 持续成长的 AI 伙伴</p>
|
<p class="sub">HoloLake · 代码守护人格体 · 持续成长的 AI 伙伴</p>
|
||||||
<div class="sc-sec">🔒 API 密钥只保存在你的浏览器本地,不会上传至任何服务器</div>
|
<div class="sc-sec">🔒 API 密钥仅本次会话有效,关闭标签页即自动清除,不会持久保存在任何地方</div>
|
||||||
|
|
||||||
<div class="fg">
|
<div class="fg">
|
||||||
<label>选择 AI 提供商</label>
|
<label>选择 AI 提供商</label>
|
||||||
<select id="sp" onchange="onProv(this.value,'s')">
|
<select id="sp" onchange="onProv(this.value,'s')">
|
||||||
|
<option value="yunwu">☁️ 云雾 AI(团队推荐)</option>
|
||||||
<option value="openai">OpenAI (ChatGPT)</option>
|
<option value="openai">OpenAI (ChatGPT)</option>
|
||||||
|
<option value="gemini">🔮 Google Gemini</option>
|
||||||
<option value="deepseek">DeepSeek(深度求索)</option>
|
<option value="deepseek">DeepSeek(深度求索)</option>
|
||||||
<option value="moonshot">Moonshot(Kimi)</option>
|
<option value="moonshot">Moonshot(Kimi)</option>
|
||||||
<option value="zhipu">智谱 AI(GLM)</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">
|
<div class="fg">
|
||||||
<label>模型</label>
|
<label>模型</label>
|
||||||
<select id="sm"></select>
|
<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>
|
||||||
<div class="fg">
|
<div class="fg">
|
||||||
<label>API 密钥</label>
|
<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="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>
|
||||||
|
|
||||||
<div class="id-section">
|
<div class="id-section">
|
||||||
|
|
@ -336,12 +351,20 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
|
||||||
|
|
||||||
<!-- ════════════════════ SETTINGS PANEL ════════════════════ -->
|
<!-- ════════════════════ SETTINGS PANEL ════════════════════ -->
|
||||||
<div id="sp2" class="sp">
|
<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="pb">
|
||||||
<div class="sfg">
|
<div class="sfg">
|
||||||
<label>AI 提供商</label>
|
<label>AI 提供商</label>
|
||||||
<select id="cp" onchange="onProv(this.value,'c')">
|
<select id="cp" onchange="onProv(this.value,'c')">
|
||||||
|
<option value="yunwu">☁️ 云雾 AI(团队推荐)</option>
|
||||||
<option value="openai">OpenAI (ChatGPT)</option>
|
<option value="openai">OpenAI (ChatGPT)</option>
|
||||||
|
<option value="gemini">🔮 Google Gemini</option>
|
||||||
<option value="deepseek">DeepSeek(深度求索)</option>
|
<option value="deepseek">DeepSeek(深度求索)</option>
|
||||||
<option value="moonshot">Moonshot(Kimi)</option>
|
<option value="moonshot">Moonshot(Kimi)</option>
|
||||||
<option value="zhipu">智谱 AI(GLM)</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">
|
<div class="sfg">
|
||||||
<label>模型</label>
|
<label>模型</label>
|
||||||
<select id="cm"></select>
|
<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>
|
||||||
<div class="sfg">
|
<div class="sfg">
|
||||||
<label>API 密钥</label>
|
<label>API 密钥</label>
|
||||||
<input type="password" id="ck" placeholder="sk-…">
|
<div style="display:flex;gap:6px;align-items:stretch">
|
||||||
<small>🔒 只保存在浏览器 localStorage,不上传任何服务器</small>
|
<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>
|
</div>
|
||||||
<button class="sbtn2" onclick="saveSet()">💾 保存设置</button>
|
<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>
|
4. 点 ⬇️ 下载单文件,放桌面直接打开使用<br>
|
||||||
5. 支持 OpenAI · DeepSeek · Kimi · 智谱 · 任意兼容接口
|
5. 支持 OpenAI · DeepSeek · Kimi · 智谱 · 任意兼容接口
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -426,7 +459,7 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
|
||||||
<ol class="help-steps">
|
<ol class="help-steps">
|
||||||
<li data-n="1" id="helpUrlStep">打开链接 <strong>https://qinfendebingshuo.github.io/guanghulab/</strong></li>
|
<li data-n="1" id="helpUrlStep">打开链接 <strong>https://qinfendebingshuo.github.io/guanghulab/</strong></li>
|
||||||
<li data-n="2"><strong>下拉菜单选择你是谁</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="4">点「开始对话」,铸渊会针对你的身份打招呼</li>
|
||||||
<li data-n="5">直接用聊天的方式问铸渊任何问题</li>
|
<li data-n="5">直接用聊天的方式问铸渊任何问题</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
@ -435,8 +468,8 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
|
||||||
<div class="help-section">
|
<div class="help-section">
|
||||||
<h3>🔑 从哪里获取 API Key?</h3>
|
<h3>🔑 从哪里获取 API Key?</h3>
|
||||||
<div class="role-guide">
|
<div class="role-guide">
|
||||||
<div class="role-guide-title">推荐用云雾 API(团队共用)</div>
|
<div class="role-guide-title">推荐用云雾 AI(团队共用)</div>
|
||||||
<p>找冰朔或肥猫获取团队 Key。填入后选「自定义」提供商,或直接用 DeepSeek。</p>
|
<p>找冰朔或肥猫获取团队 Key。填入后选「☁️ 云雾 AI(团队推荐)」提供商即可直接使用。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="role-guide">
|
<div class="role-guide">
|
||||||
<div class="role-guide-title">没有 Key?用演示模式</div>
|
<div class="role-guide-title">没有 Key?用演示模式</div>
|
||||||
|
|
@ -534,11 +567,13 @@ const ROLE_MAP = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROVS = {
|
const PROVS = {
|
||||||
openai: {base:'https://api.openai.com/v1', mdls:['gpt-4o','gpt-4o-mini','gpt-4-turbo','gpt-3.5-turbo']},
|
yunwu: {base:'https://api.yunwu.ai/v1', mdls:['gpt-4o','gpt-4o-mini','claude-3-5-sonnet-20241022','deepseek-chat','gemini-1.5-pro']},
|
||||||
deepseek: {base:'https://api.deepseek.com/v1', mdls:['deepseek-chat','deepseek-reasoner']},
|
openai: {base:'https://api.openai.com/v1', mdls:['gpt-4o','gpt-4o-mini','gpt-4-turbo','gpt-3.5-turbo']},
|
||||||
moonshot: {base:'https://api.moonshot.cn/v1', mdls:['moonshot-v1-8k','moonshot-v1-32k','moonshot-v1-128k']},
|
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']},
|
||||||
zhipu: {base:'https://open.bigmodel.cn/api/paas/v4', mdls:['glm-4','glm-4-flash','glm-4-air']},
|
deepseek: {base:'https://api.deepseek.com/v1', mdls:['deepseek-chat','deepseek-reasoner']},
|
||||||
custom: {base:'', mdls:['gpt-4o']},
|
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 = {
|
const MODES = {
|
||||||
|
|
@ -549,7 +584,11 @@ const MODES = {
|
||||||
};
|
};
|
||||||
const MODE_ORD = ['chat','build','review','brain'];
|
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%'};
|
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 KEY_MASK = '(已保存)';
|
||||||
|
|
||||||
const FB_BRAIN = {
|
const FB_BRAIN = {
|
||||||
|
|
@ -562,11 +601,17 @@ const FB_BRAIN = {
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
// STATE
|
// 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 = {
|
const A = {
|
||||||
key: ls('zy_key')||'',
|
key: sessionStorage.getItem('zy_key')||'',
|
||||||
base: ls('zy_base')||'https://api.openai.com/v1',
|
base: _initBase,
|
||||||
mdl: ls('zy_mdl')||'gpt-4o',
|
mdl: ls('zy_mdl')||DEFAULT_MDL,
|
||||||
prov: ls('zy_prov')||'openai',
|
prov: _initProv,
|
||||||
demo: false,
|
demo: false,
|
||||||
brain: null,
|
brain: null,
|
||||||
routing: null,
|
routing: null,
|
||||||
|
|
@ -585,10 +630,26 @@ const A = {
|
||||||
function ls(k){return localStorage.getItem(k)}
|
function ls(k){return localStorage.getItem(k)}
|
||||||
function lss(k,v){localStorage.setItem(k,v)}
|
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
|
// INIT
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
async function boot(){
|
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();
|
initSetupUI();
|
||||||
// Restore identity from localStorage
|
// Restore identity from localStorage
|
||||||
if(A.userName) A.userMeta = ROLE_MAP[A.userName]||null;
|
if(A.userName) A.userMeta = ROLE_MAP[A.userName]||null;
|
||||||
|
|
@ -615,24 +676,41 @@ function showApp(){
|
||||||
// SETUP FORM
|
// SETUP FORM
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
function initSetupUI(){
|
function initSetupUI(){
|
||||||
const pv = A.prov||'openai';
|
const pv = A.prov||'yunwu';
|
||||||
document.getElementById('sp').value = pv;
|
document.getElementById('sp').value = pv;
|
||||||
fillModels('sm', pv, A.mdl);
|
const isCustom = pv==='custom';
|
||||||
if(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-g').style.display='block';
|
||||||
document.getElementById('sep').value = A.base;
|
document.getElementById('sep').value = A.base;
|
||||||
|
} else {
|
||||||
|
fillModels('sm', pv, A.mdl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onProv(pv,ctx){
|
function onProv(pv,ctx){
|
||||||
const mdlSel = ctx==='s'?'sm':'cm';
|
const mdlSel = ctx==='s'?'sm':'cm';
|
||||||
const epGrp = ctx==='s'?'sep-g':'cep-g';
|
const mdlCust = ctx==='s'?'sm-cust':'cm-cust';
|
||||||
const epInp = ctx==='s'?'sep':'cep';
|
const mdlHint = ctx==='s'?'sm-cust-hint':'cm-cust-hint';
|
||||||
const cur = ctx==='s'? A.mdl : document.getElementById('cm')?.value;
|
const epGrp = ctx==='s'?'sep-g':'cep-g';
|
||||||
fillModels(mdlSel, pv, cur);
|
const epInp = ctx==='s'?'sep':'cep';
|
||||||
const show = pv==='custom';
|
const cur = ctx==='s'? A.mdl : document.getElementById('cm')?.value;
|
||||||
document.getElementById(epGrp).style.display = show?'block':'none';
|
const isCustom = pv==='custom';
|
||||||
if(!show){
|
// 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);
|
const ep = document.getElementById(epInp);
|
||||||
if(ep) ep.value = PROVS[pv]?.base||'';
|
if(ep) ep.value = PROVS[pv]?.base||'';
|
||||||
}
|
}
|
||||||
|
|
@ -641,22 +719,110 @@ function onProv(pv,ctx){
|
||||||
function fillModels(selId, pv, cur){
|
function fillModels(selId, pv, cur){
|
||||||
const sel = document.getElementById(selId);
|
const sel = document.getElementById(selId);
|
||||||
if(!sel) return;
|
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('');
|
sel.innerHTML = mdls.map(m=>`<option value="${m}">${m}</option>`).join('');
|
||||||
if(cur && mdls.includes(cur)) sel.value=cur;
|
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(){
|
function doSetup(){
|
||||||
const k = document.getElementById('sk').value.trim();
|
const k = document.getElementById('sk').value.trim();
|
||||||
const errEl = document.getElementById('sk-err');
|
const errEl = document.getElementById('sk-err');
|
||||||
if(!k){errEl.style.display='block';return;}
|
if(!k){errEl.style.display='block';return;}
|
||||||
errEl.style.display='none';
|
errEl.style.display='none';
|
||||||
const pv = document.getElementById('sp').value;
|
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||'';
|
let base = PROVS[pv]?.base||'';
|
||||||
if(pv==='custom') base=(document.getElementById('sep')?.value||'').trim()||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;
|
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
|
// Save identity
|
||||||
const un = document.getElementById('suid').value;
|
const un = document.getElementById('suid').value;
|
||||||
const gh = (document.getElementById('sghuser')?.value||'').trim();
|
const gh = (document.getElementById('sghuser')?.value||'').trim();
|
||||||
|
|
@ -676,13 +842,27 @@ function doDemo(){
|
||||||
// SETTINGS PANEL
|
// SETTINGS PANEL
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
function initSettingsPanel(){
|
function initSettingsPanel(){
|
||||||
const pv = A.prov||'openai';
|
const pv = A.prov||'yunwu';
|
||||||
document.getElementById('cp').value = pv;
|
document.getElementById('cp').value = pv;
|
||||||
fillModels('cm', pv, A.mdl);
|
const isCustom = pv==='custom';
|
||||||
document.getElementById('ck').value = A.key?KEY_MASK:'';
|
document.getElementById('cm').style.display = isCustom?'none':'block';
|
||||||
if(pv==='custom'){
|
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-g').style.display='block';
|
||||||
document.getElementById('cep').value = A.base;
|
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.userName) document.getElementById('cuid').value=A.userName;
|
||||||
if(A.ghUser) document.getElementById('cghuser').value=A.ghUser;
|
if(A.ghUser) document.getElementById('cghuser').value=A.ghUser;
|
||||||
|
|
@ -690,13 +870,19 @@ function initSettingsPanel(){
|
||||||
|
|
||||||
function saveSet(){
|
function saveSet(){
|
||||||
const pv = document.getElementById('cp').value;
|
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 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||'';
|
let base = PROVS[pv]?.base||'';
|
||||||
if(pv==='custom') base=(document.getElementById('cep').value.trim())||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;
|
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
|
// Save identity
|
||||||
const un = document.getElementById('cuid')?.value||A.userName;
|
const un = document.getElementById('cuid')?.value||A.userName;
|
||||||
const gh = document.getElementById('cghuser')?.value?.trim()||A.ghUser;
|
const gh = document.getElementById('cghuser')?.value?.trim()||A.ghUser;
|
||||||
|
|
@ -705,7 +891,18 @@ function saveSet(){
|
||||||
document.getElementById('bbdemo').style.display=A.demo?'inline':'none';
|
document.getElementById('bbdemo').style.display=A.demo?'inline':'none';
|
||||||
applyIdentityUI();
|
applyIdentityUI();
|
||||||
closeP();
|
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){
|
if(!res.ok){
|
||||||
const provName=PROVS[A.prov]?.base||A.base;
|
const provLabel = A.prov==='custom' ? A.base : (A.prov+'('+A.base+')');
|
||||||
let em=provName+' 返回错误 '+res.status;
|
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(_){}
|
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='';
|
let full='';
|
||||||
const rdr=res.body.getReader();
|
const rdr=res.body.getReader();
|
||||||
|
|
@ -942,7 +1142,17 @@ async function streamReply(txt){
|
||||||
A.msgs.push({role:'assistant',content:full});
|
A.msgs.push({role:'assistant',content:full});
|
||||||
addCopyBtns(el);
|
addCopyBtns(el);
|
||||||
}catch(e){
|
}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();
|
A.msgs.pop();
|
||||||
}finally{
|
}finally{
|
||||||
A.streaming=false; updSendBtn(); scrollB();
|
A.streaming=false; updSendBtn(); scrollB();
|
||||||
|
|
@ -1170,7 +1380,9 @@ function closeP(){
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
// COPY URL (share link)
|
// 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){
|
function showCopyFeedback(success){
|
||||||
const btn=document.querySelector('.copy-url-btn');
|
const btn=document.querySelector('.copy-url-btn');
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"name": "nextjs",
|
"name": "nextjs",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "15.3.2",
|
"next": "15.3.8",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1511,15 +1511,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "15.3.2",
|
"version": "15.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.8.tgz",
|
||||||
"integrity": "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g==",
|
"integrity": "sha512-SAfHg0g91MQVMPioeFeDjE+8UPF3j3BvHjs8ZKJAUz1BG7eMPvfCKOAgNWJ6s1MLNeP6O2InKQRTNblxPWuq+Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
"version": "15.3.2",
|
"version": "15.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.5.tgz",
|
||||||
"integrity": "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g==",
|
"integrity": "sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1533,9 +1533,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
"version": "15.3.2",
|
"version": "15.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.5.tgz",
|
||||||
"integrity": "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w==",
|
"integrity": "sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1549,9 +1549,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
"version": "15.3.2",
|
"version": "15.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.5.tgz",
|
||||||
"integrity": "sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA==",
|
"integrity": "sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1565,9 +1565,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
"version": "15.3.2",
|
"version": "15.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.5.tgz",
|
||||||
"integrity": "sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg==",
|
"integrity": "sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1581,9 +1581,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
"version": "15.3.2",
|
"version": "15.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.5.tgz",
|
||||||
"integrity": "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==",
|
"integrity": "sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1597,9 +1597,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
"version": "15.3.2",
|
"version": "15.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.5.tgz",
|
||||||
"integrity": "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==",
|
"integrity": "sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1613,9 +1613,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
"version": "15.3.2",
|
"version": "15.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.5.tgz",
|
||||||
"integrity": "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==",
|
"integrity": "sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1629,9 +1629,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "15.3.2",
|
"version": "15.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.5.tgz",
|
||||||
"integrity": "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA==",
|
"integrity": "sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1917,6 +1917,66 @@
|
||||||
"node": ">=14.0.0"
|
"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": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.5.tgz",
|
||||||
|
|
@ -4575,12 +4635,12 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "15.3.2",
|
"version": "15.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-15.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-15.3.8.tgz",
|
||||||
"integrity": "sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ==",
|
"integrity": "sha512-L+4c5Hlr84fuaNADZbB9+ceRX9/CzwxJ+obXIGHupboB/Q1OLbSUapFs4bO8hnS/E6zV/JDX7sG1QpKVR2bguA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "15.3.2",
|
"@next/env": "15.3.8",
|
||||||
"@swc/counter": "0.1.3",
|
"@swc/counter": "0.1.3",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
"busboy": "1.6.0",
|
"busboy": "1.6.0",
|
||||||
|
|
@ -4595,14 +4655,14 @@
|
||||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@next/swc-darwin-arm64": "15.3.2",
|
"@next/swc-darwin-arm64": "15.3.5",
|
||||||
"@next/swc-darwin-x64": "15.3.2",
|
"@next/swc-darwin-x64": "15.3.5",
|
||||||
"@next/swc-linux-arm64-gnu": "15.3.2",
|
"@next/swc-linux-arm64-gnu": "15.3.5",
|
||||||
"@next/swc-linux-arm64-musl": "15.3.2",
|
"@next/swc-linux-arm64-musl": "15.3.5",
|
||||||
"@next/swc-linux-x64-gnu": "15.3.2",
|
"@next/swc-linux-x64-gnu": "15.3.5",
|
||||||
"@next/swc-linux-x64-musl": "15.3.2",
|
"@next/swc-linux-x64-musl": "15.3.5",
|
||||||
"@next/swc-win32-arm64-msvc": "15.3.2",
|
"@next/swc-win32-arm64-msvc": "15.3.5",
|
||||||
"@next/swc-win32-x64-msvc": "15.3.2",
|
"@next/swc-win32-x64-msvc": "15.3.5",
|
||||||
"sharp": "^0.34.1"
|
"sharp": "^0.34.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
"broadcast:distribute": "node scripts/distribute-broadcasts.js"
|
"broadcast:distribute": "node scripts/distribute-broadcasts.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "15.3.2",
|
"next": "15.3.8",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^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('✅ 大脑文件完整性:全部就绪');
|
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 seen = new Set();
|
||||||
const uniqueFaq = kb.faq.filter(item => {
|
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