V3.5 精细化运营升级:用量统计/接口库/团队/对话/工作目录/流式超时
1. 精细化统计:cost_records 新增 calls/cached_tokens/latency_ms/first_token_ms;
用量明细报表(项目×智能体矩阵) + 成本报表细化(输入/输出/缓存命中/调用次数)
2. 从参考项目中新建:内置3个测试项目(文案/Python/调研),一键复制目标+任务
3. 大模型接口库(llm_endpoints):专门配置接口(地址/密钥/模型/定价),
计费支持按token(逐模型)与按调用次数;创建AI Worker直接选用;
AI Worker团队(worker_teams):打包Worker,对话/建项目可直接选团队
4. 对话导航融合仪表盘:可选大模型/AI Worker/团队,默认主力AI Worker(⭐可设),SSE流式
5. 系统工作目录:默认data/workspace可改绝对路径;项目与多Agent协作均在其下
建唯一工作目录;手动输入目录已存在则列出信息并需手动确认
6. 任务执行超时改为流式单token返回超时+首字延迟超时,设置页可配;
所有模型输出SSE按token接收
This commit is contained in:
@@ -390,6 +390,66 @@ CREATE INDEX IF NOT EXISTS idx_cost_project ON cost_records(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_docs_project ON documents(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_chunks_doc ON doc_chunks(document_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_alerts_read ON alerts(read);
|
||||
|
||||
-- ===================================================================
|
||||
-- V3.5 表结构:大模型接口库 / AI Worker 团队 / 对话 / 精细化计量
|
||||
-- ===================================================================
|
||||
CREATE TABLE IF NOT EXISTS llm_endpoints (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
provider TEXT DEFAULT 'custom', -- doubao/deepseek/autodl/qwen/openai/vllm/custom
|
||||
base_url TEXT DEFAULT '',
|
||||
api_key TEXT DEFAULT '',
|
||||
models TEXT DEFAULT '[]', -- JSON: 可用模型名列表
|
||||
pricing TEXT DEFAULT '{}', -- JSON: {model: {input: 元/1M, output: 元/1M}}
|
||||
input_price REAL DEFAULT 0, -- 兜底输入价(元/1M tokens)
|
||||
output_price REAL DEFAULT 0, -- 兜底输出价(元/1M tokens)
|
||||
price_per_call REAL DEFAULT 0, -- 按调用次数计费单价(元/次)
|
||||
billing TEXT DEFAULT 'token', -- token=按token数计费 / call=按调用次数计费
|
||||
description TEXT DEFAULT '',
|
||||
status TEXT DEFAULT 'enabled', -- enabled/disabled
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS worker_teams (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
worker_ids TEXT DEFAULT '[]', -- JSON: worker id 列表
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chat_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT DEFAULT '',
|
||||
target_type TEXT DEFAULT 'worker', -- model=大模型接口 / worker=AI Worker / team=团队
|
||||
target_id INTEGER DEFAULT 0,
|
||||
model TEXT DEFAULT '', -- target_type=model 时选定的模型名
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chat_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id INTEGER NOT NULL,
|
||||
role TEXT DEFAULT 'user', -- user/assistant
|
||||
content TEXT DEFAULT '',
|
||||
model TEXT DEFAULT '',
|
||||
worker_id INTEGER,
|
||||
prompt_tokens INTEGER DEFAULT 0,
|
||||
completion_tokens INTEGER DEFAULT 0,
|
||||
cached_tokens INTEGER DEFAULT 0,
|
||||
cost REAL DEFAULT 0,
|
||||
latency_ms INTEGER DEFAULT 0,
|
||||
first_token_ms INTEGER DEFAULT 0,
|
||||
error TEXT DEFAULT '',
|
||||
created_at INTEGER
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_msg_session ON chat_messages(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_worker ON cost_records(worker_id);
|
||||
"""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -468,6 +528,86 @@ def _migrate():
|
||||
for r in conn.execute("SELECT id, workspace_dir FROM projects WHERE workspace_dir IS NULL OR workspace_dir=''"):
|
||||
conn.execute('UPDATE projects SET workspace_dir=? WHERE id=?',
|
||||
('project_%d' % r['id'], r['id']))
|
||||
|
||||
# ================= V3.5 迁移:精细化计量 / 接口库 / 团队 / 对话 =================
|
||||
ccols = {r['name'] for r in conn.execute('PRAGMA table_info(cost_records)')}
|
||||
for col, ddl in (
|
||||
('calls', 'ALTER TABLE cost_records ADD COLUMN calls INTEGER DEFAULT 1'),
|
||||
('cached_tokens', 'ALTER TABLE cost_records ADD COLUMN cached_tokens INTEGER DEFAULT 0'),
|
||||
('latency_ms', 'ALTER TABLE cost_records ADD COLUMN latency_ms INTEGER DEFAULT 0'),
|
||||
('first_token_ms', 'ALTER TABLE cost_records ADD COLUMN first_token_ms INTEGER DEFAULT 0'),
|
||||
):
|
||||
if col not in ccols:
|
||||
conn.execute(ddl)
|
||||
wcols = {r['name'] for r in conn.execute('PRAGMA table_info(workers)')}
|
||||
for col, ddl in (
|
||||
('endpoint_id', 'ALTER TABLE workers ADD COLUMN endpoint_id INTEGER'),
|
||||
('is_main', 'ALTER TABLE workers ADD COLUMN is_main INTEGER DEFAULT 0'),
|
||||
):
|
||||
if col not in wcols:
|
||||
conn.execute(ddl)
|
||||
pcols3 = {r['name'] for r in conn.execute('PRAGMA table_info(projects)')}
|
||||
if 'is_reference' not in pcols3:
|
||||
conn.execute('ALTER TABLE projects ADD COLUMN is_reference INTEGER DEFAULT 0')
|
||||
acols = {r['name'] for r in conn.execute('PRAGMA table_info(agent_runs)')}
|
||||
if 'workspace_dir' not in acols:
|
||||
conn.execute("ALTER TABLE agent_runs ADD COLUMN workspace_dir TEXT DEFAULT ''")
|
||||
scols = {r['name'] for r in conn.execute('PRAGMA table_info(chat_sessions)')}
|
||||
if 'model' not in scols:
|
||||
conn.execute("ALTER TABLE chat_sessions ADD COLUMN model TEXT DEFAULT ''")
|
||||
# V3.5 新表(幂等)
|
||||
conn.execute('''CREATE TABLE IF NOT EXISTS llm_endpoints (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
provider TEXT DEFAULT 'custom',
|
||||
base_url TEXT DEFAULT '',
|
||||
api_key TEXT DEFAULT '',
|
||||
models TEXT DEFAULT '[]',
|
||||
pricing TEXT DEFAULT '{}',
|
||||
input_price REAL DEFAULT 0,
|
||||
output_price REAL DEFAULT 0,
|
||||
price_per_call REAL DEFAULT 0,
|
||||
billing TEXT DEFAULT 'token',
|
||||
description TEXT DEFAULT '',
|
||||
status TEXT DEFAULT 'enabled',
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER
|
||||
)''')
|
||||
conn.execute('''CREATE TABLE IF NOT EXISTS worker_teams (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
worker_ids TEXT DEFAULT '[]',
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER
|
||||
)''')
|
||||
conn.execute('''CREATE TABLE IF NOT EXISTS chat_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT DEFAULT '',
|
||||
target_type TEXT DEFAULT 'worker',
|
||||
target_id INTEGER DEFAULT 0,
|
||||
model TEXT DEFAULT '',
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER
|
||||
)''')
|
||||
conn.execute('''CREATE TABLE IF NOT EXISTS chat_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id INTEGER NOT NULL,
|
||||
role TEXT DEFAULT 'user',
|
||||
content TEXT DEFAULT '',
|
||||
model TEXT DEFAULT '',
|
||||
worker_id INTEGER,
|
||||
prompt_tokens INTEGER DEFAULT 0,
|
||||
completion_tokens INTEGER DEFAULT 0,
|
||||
cached_tokens INTEGER DEFAULT 0,
|
||||
cost REAL DEFAULT 0,
|
||||
latency_ms INTEGER DEFAULT 0,
|
||||
first_token_ms INTEGER DEFAULT 0,
|
||||
error TEXT DEFAULT '',
|
||||
created_at INTEGER
|
||||
)''')
|
||||
conn.execute('CREATE INDEX IF NOT EXISTS idx_chat_msg_session ON chat_messages(session_id)')
|
||||
conn.execute('CREATE INDEX IF NOT EXISTS idx_cost_worker ON cost_records(worker_id)')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -524,6 +664,8 @@ def init_db():
|
||||
_migrate()
|
||||
migrate_v2()
|
||||
seed_builtin_roles()
|
||||
seed_endpoints()
|
||||
link_workers_endpoints()
|
||||
|
||||
|
||||
# V3.1 内置角色权限点定义(admin 为特殊值 ALL,表示全部权限)
|
||||
@@ -573,6 +715,87 @@ def recover_stale_runs():
|
||||
return (n1 or 0, n2 or 0, n3 or 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# V3.5 大模型接口库:从 config.PROVIDERS/MODEL_PRICING 自动导入 + 存量 Worker 关联
|
||||
# ---------------------------------------------------------------------------
|
||||
def seed_endpoints():
|
||||
"""幂等:首次启动把 config.PROVIDERS 导入为大模型接口库(含逐模型定价),
|
||||
便于在页面上统一管理与配置价格(按 token / 按调用次数)。"""
|
||||
c = q('SELECT COUNT(*) c FROM llm_endpoints')[0]['c']
|
||||
if c > 0:
|
||||
return
|
||||
import config as cfg
|
||||
prefix = {'doubao': 'doubao', 'deepseek': 'deepseek', 'openai': 'gpt',
|
||||
'qwen': 'qwen', 'vllm': '', 'autodl': 'qwen3'}
|
||||
ts = now()
|
||||
for pid, p in cfg.PROVIDERS.items():
|
||||
models = [m for m in cfg.MODEL_PRICING if m.startswith(prefix.get(pid, '__none__'))]
|
||||
pricing = {m: cfg.MODEL_PRICING[m] for m in models}
|
||||
fp = cfg.MODEL_PRICING.get(models[0]) if models else cfg.DEFAULT_PRICE
|
||||
w('INSERT INTO llm_endpoints (name, provider, base_url, api_key, models, pricing, '
|
||||
'input_price, output_price, price_per_call, billing, description, status, created_at, updated_at) '
|
||||
'VALUES (?,?,?,?,?,?,?,?,0,?,?,?,?,?)',
|
||||
(p['name'], pid, p['base_url'], p['api_key'], json.dumps(models), json.dumps(pricing),
|
||||
fp.get('input', 2.0), fp.get('output', 8.0), 'token',
|
||||
f'由系统配置自动导入({pid})', 'enabled', ts, ts))
|
||||
|
||||
|
||||
def link_workers_endpoints():
|
||||
"""存量 Worker:provider 匹配的接口库自动关联 endpoint_id(统一计价与鉴权)"""
|
||||
conn = get_conn()
|
||||
try:
|
||||
eps = {r['provider']: r['id'] for r in conn.execute('SELECT id, provider FROM llm_endpoints')}
|
||||
for r in conn.execute('SELECT id, provider FROM workers WHERE endpoint_id IS NULL OR endpoint_id=0'):
|
||||
eid = eps.get(r['provider'])
|
||||
if eid:
|
||||
conn.execute('UPDATE workers SET endpoint_id=? WHERE id=?', (eid, r['id']))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def main_worker_id():
|
||||
"""主力 AI Worker id:优先取设置 main_worker_id 指向的启用 Worker,否则首个启用 Worker"""
|
||||
mid = get_setting('main_worker_id', '')
|
||||
if mid:
|
||||
r = q('SELECT id FROM workers WHERE id=? AND status="enabled"', (int(mid),), one=True)
|
||||
if r:
|
||||
return r['id']
|
||||
r = q('SELECT id FROM workers WHERE status="enabled" ORDER BY id', one=True)
|
||||
return r['id'] if r else None
|
||||
|
||||
|
||||
def set_main_worker(worker_id):
|
||||
"""设置主力 AI Worker:先清除其它 is_main,再标记目标"""
|
||||
conn = get_conn()
|
||||
try:
|
||||
conn.execute('UPDATE workers SET is_main=0 WHERE is_main=1')
|
||||
conn.execute('UPDATE workers SET is_main=1 WHERE id=?', (int(worker_id),))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
set_setting('main_worker_id', int(worker_id))
|
||||
|
||||
|
||||
def get_llm_timeouts():
|
||||
"""读取超时配置:token_timeout(单token返回超时) / first_token_timeout(首字延迟超时) / request_timeout(整体兜底)。
|
||||
单位秒,0/空 回退 config 默认。"""
|
||||
import config as cfg
|
||||
try:
|
||||
tk = float(get_setting('token_timeout', '') or 0) or cfg.TOKEN_TIMEOUT
|
||||
except Exception:
|
||||
tk = cfg.TOKEN_TIMEOUT
|
||||
try:
|
||||
fk = float(get_setting('first_token_timeout', '') or 0) or cfg.FIRST_TOKEN_TIMEOUT
|
||||
except Exception:
|
||||
fk = cfg.FIRST_TOKEN_TIMEOUT
|
||||
try:
|
||||
rk = float(get_setting('request_timeout', '') or 0) or cfg.TASK_TIMEOUT
|
||||
except Exception:
|
||||
rk = cfg.TASK_TIMEOUT
|
||||
return max(3.0, tk), max(3.0, fk), max(10.0, rk)
|
||||
|
||||
|
||||
def q(sql, args=(), one=False):
|
||||
"""查询"""
|
||||
conn = get_conn()
|
||||
|
||||
Reference in New Issue
Block a user