Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
- 企业:管理员配 key → 分发子 key 给员工
- 公共:分享者上传 key → 进共享池
- key 状态:可用/失效/限额/撤销
- **管理入口(v1.11 定案)**:无独立 Key 池管理界面——管理员与普通用户一样通过**「上架」**(共享管理页:选厂商 → Plan → 模型、填 key、声明额度、可用时间段)配置上游 key;共享列表即 key 池视图(可暂停 / 删除)。「Key 池」保留为数据层概念(平台持有的上游 key 集合),UI 侧不再单独暴露。

#### 4.2.1 路由与故障转移策略(v0.2,宿主 2026-08-14 定案)

Expand Down
9 changes: 5 additions & 4 deletions docs/user-stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

> **本文档以中文撰写**(用户故事主体、验收标准、流程说明均为中文;关键术语保留中英对照)。
>
> **v1.11(2026-08-17)· 管理视图移除独立「Key 池」管理,统一走「上架 key」.** 管理员与普通用户一样通过**共享管理页「上架新 key」**(选厂商 → Plan → 模型、填 key、声明额度、可用时间段)配置上游 key——企业场景下管理员上架、员工消费,本质上就是 key 池;不再有独立的管理员 Key 池界面。移除 Admin「Key 池」tab(成员管理 / 用量报表 / 组织管理 / 平台运营保留)。「Key 池」保留为**数据层概念**(平台持有的上游 key 集合,见 docs/architecture.md 4.2)。更新 US-12 / 企业流程 / 对齐表。
> **v1.10(2026-08-17)· 上架表单"可用时间段"结构化.** 上架 key 的可用时间段改为**正式字段**(星期多选 + 起止时间,可多选;留空 = 全天不限),后端可解析生效;**备注只作纯文本**,不承载任何逻辑。共享列表展示可用时间段(如「周一~周五 09:00-18:00」),未设置显示「全天」。更新 US-8 / US-9 AC。
> **v1.9(2026-08-17)· 上架流程改为「厂商 → Plan → 模型」.** 分享者上架时选择的是**厂商的 Plan**(而不是厂商/模型):大多数分享的是自己的 Plan 订阅(如阿里 Token Plan、Kimi Code),同一厂商不同 Plan 的 Base URL 可能不同。上架表单改为三级联动「选厂商 → 选 Plan → 选该 Plan 支持的模型」,Plan 下拉中「API(按量)」代表按量计价的 key,其余为订阅 Plan;内置国内已知 Plan 清单(阿里云百炼 / 智谱 / 火山方舟 / Kimi 月之暗面 / MiniMax / DeepSeek,每家至少一个「API(按量)」项)。共享列表展示「厂商 · Plan / 模型」。更新 US-8 / US-9 / 核心机制表「上架 Listing」行。
> **v1.8(2026-08-17)· 移除面向用户文案中的防薅表述,文档措辞中性化.** 「防薅羊毛」是产品内部设计动机,不应出现在面向用户的界面文案中(UI 侧已同步移除)。本文档中的设计备注措辞中性化:「防薅动机」改为「设计意图」(如每日赠送机制的设计意图是**鼓励每日活跃**,同时降低集中注册多号的收益)。面向用户可见的赠送规则保持「每日赠送 1 点(当日有效)· 连续 10 天」。更新核心机制表「赠送点数」行措辞。
Expand Down Expand Up @@ -233,8 +234,8 @@ flowchart LR

#### 用户故事(User Stories)

- **US-12** 作为企业管理员,我希望把上游 key 加入 key 池,以便成员能使用已购模型。
- AC:管理视图 → Key 池:添加 key(厂商 / 模型 / key / 配额);key 脱敏展示;状态有 ok / limit / exhausted / revoked;撤销立即禁用该 key。
- **US-12** 作为企业管理员,我希望通过「上架 key」配置上游 key(与普通用户同一入口 / 界面),以便成员能使用已购模型。
- AC:管理员在共享管理页「上架新 key」(选厂商 → Plan → 模型、填 key、声明额度、可用时间段)配置上游 key;key 脱敏展示;共享列表即 key 池视图,可暂停 / 删除(撤销);无独立的管理员 Key 池界面。
- **US-13** 作为企业管理员,我希望管理部门(CRUD + 每月点数分配),以便按组织结构分配预算。
- AC:组织 Tab:部门列表(成员数、每月分配、已用、剩余、状态);部门增删改查;删除部门后其成员变为「未分配」——见 E-03。
- **US-14** 作为企业管理员,我希望管理成员(充值 / 改部门),以便个人获得正确的配额与权限。
Expand All @@ -258,7 +259,7 @@ flowchart LR
```

1. 管理员登录并打开管理视图(角色门控)(US-12)。
2. 把上游 key 加入 key 池;撤销异常 key(US-12)。
2. 通过共享管理页「上架新 key」配置上游 key;暂停 / 删除即撤销(US-12)。
3. 创建部门并分配每月点数(US-13)。
4. 把成员分配到部门 / 充值;新成员默认未分配(US-14)。
5. 员工登录,看到分配配额(US-17),通过市场消费(US-18)。
Expand Down Expand Up @@ -352,7 +353,7 @@ flowchart LR
| 点数与定价 | `ui/js/data.js`(**1 点 ≈ 1 元 CNY**;CNY 模型直接 1 元 = 1 点,USD 模型按 ~7.2 汇率折算;参考价 = 输出价 点数/1M) | US-1、US-2、US-8 使用同一套规则;没有分享者自定定价;消费点数可为小数(保留 2 位)。 |
| 点数有效期与扣减顺序 | `docs/user-stories.md` v1.7(每日赠送 1 点 / 1 天有效 / 连续 10 天;收益与充值永久;先扣赠送;金额保留 2 位小数;分享收益扣 10% 平台分成) | US-22、US-23、US-24、E-13、E-14、E-15、E-16 一致;UI mock 显示今日赠送(+1)、有效期与连续天数计数,收益按 90% 计入。 |
| Key 脱敏与安全 | `ui/js/data.js`、`ui/README.md`(脱敏 key、加密托管、删除确认) | US-8、US-9、US-21、E-05 一致。 |
| 企业语义 | `ui/js/data.js`(key 池状态、部门、成员未分配) | US-12…US-16、E-03、E-06 一致。 |
| 企业语义 | `ui/js/data.js`(上架 key(SHARINGS)、部门、成员未分配;无独立 key 池管理) | US-12…US-16、E-03、E-06 一致。 |
| 集中式架构 | `docs/architecture.md`(平台托管 key、计量引擎、账本) | 全文基于集中式执行假设;消费 / 收益都流经平台账本。 |
| 交易记录唯一明细 | PR #8(钱包 / 交易记录去重) | US-7:交易记录页是唯一明细入口;钱包页链接过去。 |

Expand Down
2 changes: 1 addition & 1 deletion ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ python3 -m http.server 8000 --directory ui
4. 钱包 Wallet — 点数余额、**本月点数变化(近 1 月按类型汇总收支 + 净变化,与仪表盘一致)**、**充值入口(US-4:模拟流程——输入点数 → 余额增加 → topup 交易记录;文案注明"演示,真实支付后续接入";提现仍 disabled)**、**申请加额(US-20:企业成员余额低时申请更多点数 → 提交后等待管理员审批,与组织设置开关联动)**;收支明细已去重,统一到【交易记录】(页内提供跳转提示)
5. 交易记录 Transactions — 消费/收益/充值/提现/赠送(gift)唯一明细入口,Tab 筛选 + MRT 风格表格(列排序/列筛选/分页,与 Tab 叠加生效)
6. 设置 Settings — 账户、API Key 管理(完整增删改查:生成带名字、改名、删除需确认「删除后该 key 立即失效」、按名字搜索、一键复制完整 key——列表脱敏展示 atk_live_****xxxx,复制为完整值;file:// 下 clipboard API 受限时降级为选中+Ctrl/Cmd+C)、通知、偏好
7. 管理视图 Admin(管理员 / 运营者角色专属)— Key 池管理(添加/撤销)、成员管理(改部门:下拉可选部门或"未分配";任意金额充值,正整数校验;新注册成员默认未分配部门;**加额申请审批(US-20:待审批列表 → 批准=成员余额+申请点数 / 驳回)**)、用量报表(按模型/成员)、组织管理(部门列表 + 部门增删改查 + 每月点数分配,含部门汇总统计,未分配成员不计入任何部门;保留组织设置卡片,含"关闭外部注册"企业部署开关与"成员自助申请加额需管理员审批"开关——关掉后成员申请直接生效)、平台运营(运营者 = 宿主本人,职责最小化:① 运行概览——运行状态 / 用户数 / 共享 key 数 / 交易量 / 点数流入流出;② 用户充值——按用户名 / 邮箱搜索定位,输入点数金额(可为小数)确认后余额增加永久有效点数并产生一条交易记录)
7. 管理视图 Admin(管理员 / 运营者角色专属)— 成员管理(改部门:下拉可选部门或"未分配";任意金额充值,正整数校验;新注册成员默认未分配部门;**加额申请审批(US-20:待审批列表 → 批准=成员余额+申请点数 / 驳回)**)、用量报表(按模型/成员)、组织管理(部门列表 + 部门增删改查 + 每月点数分配,含部门汇总统计,未分配成员不计入任何部门;保留组织设置卡片,含"关闭外部注册"企业部署开关与"成员自助申请加额需管理员审批"开关——关掉后成员申请直接生效)、平台运营(运营者 = 宿主本人,职责最小化:① 运行概览——运行状态 / 用户数 / 共享 key 数 / 交易量 / 点数流入流出;② 用户充值——按用户名 / 邮箱搜索定位,输入点数金额(可为小数)确认后余额增加永久有效点数并产生一条交易记录)。**无独立 Key 池管理**——管理员与普通用户一样通过共享管理页「上架 key」配置上游 key(共享列表即 key 池视图,可暂停/删除)

## 文件结构

Expand Down
22 changes: 3 additions & 19 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -289,32 +289,16 @@ <h3>偏好 <span class="en">Preferences</span></h3>
<section class="view hidden" id="view-admin">
<header class="page-head">
<h2>管理视图 <span class="en">Admin</span></h2>
<p>管理员角色专属:Key 池 · 成员/员工点数 · 用量报表 · 组织管理(普通用户无此视图)</p>
<p>管理员角色专属:成员/员工点数 · 用量报表 · 组织管理 · 平台运营(普通用户无此视图)</p>
</header>
<div class="tabs" id="admin-tabs">
<button class="tab active" data-admin-tab="keys">Key 池</button>
<button class="tab" data-admin-tab="employees">成员管理</button>
<button class="tab active" data-admin-tab="employees">成员管理</button>
<button class="tab" data-admin-tab="usage">用量报表</button>
<button class="tab" data-admin-tab="org">组织管理</button>
<button class="tab" data-admin-tab="ops">平台运营 <span class="en">Operator</span></button>
</div>

<div class="admin-pane" data-admin-pane="keys">
<div class="toolbar">
<input id="ak-search" class="input" placeholder="搜索 key / 厂商 / 模型…">
<button class="btn btn-primary" id="add-key-btn">添加上游 Key <span class="en">(+ Add key)</span></button>
</div>
<div class="card">
<div class="table-wrap compact">
<table class="table">
<thead><tr><th>厂商</th><th>模型</th><th>Key</th><th>额度(点数)</th><th>状态</th><th></th></tr></thead>
<tbody id="keys-body"></tbody>
</table>
</div>
</div>
</div>

<div class="admin-pane hidden" data-admin-pane="employees">
<div class="admin-pane" data-admin-pane="employees">
<div class="stat-grid" id="emp-stats"></div>
<div class="card">
<div class="table-wrap compact">
Expand Down
40 changes: 1 addition & 39 deletions ui/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -727,29 +727,11 @@

/* --- 管理员角色视图 --- */

const KEY_STATUS = {
ok: { text: "可用", cls: "ok" },
limit: { text: "达限额", cls: "warn" },
exhausted: { text: "已用尽", cls: "danger" },
revoked: { text: "已撤销", cls: "dim" },
};

function renderAdmin() {
const tab = $("#admin-tabs .tab.active").dataset.adminTab;
$$(".admin-pane").forEach((p) => p.classList.toggle("hidden", p.dataset.adminPane !== tab));

if (tab === "keys") {
const q = ($("#ak-search").value || "").toLowerCase();
const rows = D.KEYS.filter((k) => !q || k.model.includes(q) || k.provider.includes(q) || k.key.includes(q));
$("#keys-body").innerHTML = rows.map((k) =>
"<tr><td>" + esc(k.provider) + "</td><td><strong>" + esc(k.model) + "</strong></td>" +
"<td><code>" + esc(k.key) + "</code></td>" +
"<td>" + D.fmt(k.used) + " / " + D.fmt(k.quota) + "</td>" +
"<td>" + badge(k.status, KEY_STATUS) + "</td>" +
"<td><button class='btn btn-ghost' style='padding:4px 10px;font-size:12px' data-key-revoke='" + k.id + "'>" +
(k.status === "revoked" ? "恢复" : "撤销") + "</button></td></tr>"
).join("");
} else if (tab === "employees") {
if (tab === "employees") {
const total = D.EMPLOYEES.reduce((a, e) => a + e.quota, 0);
const used = D.EMPLOYEES.reduce((a, e) => a + e.used, 0);
const unassigned = D.EMPLOYEES.filter((e) => !e.dept).length;
Expand Down Expand Up @@ -1202,26 +1184,6 @@
renderAdmin();
}));

$("#add-key-btn").addEventListener("click", () => {
const m = D.MODELS[Math.floor(Math.random() * D.MODELS.length)];
D.KEYS.unshift({ id: Date.now(), provider: m.provider, model: m.model, key: "sk-****-new1", quota: 100000, used: 0, status: "ok" });
renderAdmin();
toast("已添加上游 Key(演示:" + m.model + ")");
});

$("#ak-search").addEventListener("input", renderAdmin);

// 管理台事件委托(撤销 key / 员工充值)
$("#keys-body").addEventListener("click", (e) => {
const b = e.target.closest("[data-key-revoke]");
if (!b) return;
const k = D.KEYS.find((x) => x.id === Number(b.dataset.keyRevoke));
if (!k) return;
k.status = k.status === "revoked" ? "ok" : "revoked";
renderAdmin();
toast(k.status === "revoked" ? "已撤销 " + k.model + " 的 key" : "已恢复 " + k.model + " 的 key");
});

$("#emp-body").addEventListener("click", (e) => {
const dd = e.target.closest("[data-emp-dept]");
if (dd) { editEmpDept(Number(dd.dataset.empDept)); return; }
Expand Down
10 changes: 0 additions & 10 deletions ui/js/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,6 @@

// —— 企业版 ——

// Key 池
KEYS: [
{ id: 1, provider: "openai", model: "gpt-5.5-pro", key: "sk-****-8f2a", quota: 300000, used: 214000, status: "ok" },
{ id: 2, provider: "anthropic",model: "claude-opus-4.7", key: "sk-****-1c77", quota: 200000, used: 62000, status: "ok" },
{ id: 3, provider: "deepseek", model: "deepseek-v4-flash", key: "sk-****-90bb", quota: 500000, used: 461000, status: "limit" },
{ id: 4, provider: "zhipu", model: "glm-5.2", key: "sk-****-44e9", quota: 150000, used: 150000, status: "exhausted" },
{ id: 5, provider: "google", model: "gemini-3.1-pro", key: "sk-****-77d2", quota: 180000, used: 15000, status: "ok" },
{ id: 6, provider: "moonshot", model: "kimi-k3", key: "sk-****-ab31", quota: 100000, used: 0, status: "revoked" },
],

// 员工(dept 可为空 = 未分配部门,如新注册未安排)
EMPLOYEES: [
{ id: 1, name: "陈默", dept: "研发", quota: 20000, used: 12400 },
Expand Down
Loading