init(baseline): import AIOS V1R1C0 multiuser gitops custom llm baseline

This commit is contained in:
2026-07-06 14:44:09 +08:00
commit 6bdd44b25b
101 changed files with 3094 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
.env
*.secret
*.key
*.pem
*.log
__pycache__/
node_modules/
.venv/
.DS_Store
runtime/tmp/
runtime/cache/
logs/
+94
View File
@@ -0,0 +1,94 @@
e4a1f86ef92ec1d3ff724eb75cf9c01758c1cc997e1afadc1c8b561bce5b813a FULL_PACKAGE_MANIFEST.json
c59c0288d4d999fbd79a2f275d4fb8c369029cb8bb721f95408308cc51eaa3a2 README.md
1095953d5764d9c72f02e84b1d525e265d3a67a5bdb451ab9ef51a8e8b3032b8 VERSION
ee6e4fc6bc98a590fbc3585b62707e2562115e5a3f722e69a7c464d966bdc458 active-runtime/app/aios_gitops_model_api.py
0ffadf6ee1f297842bc44c6e753010fd2b7b02b1f5fb0d7e0676b7bec898db08 active-runtime/app/aios_v5_webops_app.py
0ffadf6ee1f297842bc44c6e753010fd2b7b02b1f5fb0d7e0676b7bec898db08 active-runtime/app/aios_webops_app.py
0eb12e0161371464605577707c84df413b555277933f9eb4488ab38032ff699b active-runtime/migrations/001_webops_core_sqlite.sql
6d61da88e7d6b208e7eee7195b521dc47cb68587f91987d7a0dd1c8801ef168d active-runtime/migrations/002_seed_owner_admin_user.sql
fcd407fca66d7100242d9231f315ce248dde4c5b6a0679eca923f6fe0d0188eb active-runtime/migrations/003_v5_digital_life_core.sql
a2c0c7699e1eb46cfdeabfc905cb79b71ed14fb6bcf81ecc99ec47975cf05767 active-runtime/migrations/004_gitops_custom_llm_runtime.sql
ee6e4fc6bc98a590fbc3585b62707e2562115e5a3f722e69a7c464d966bdc458 adapters/gitops/aios_gitops_model_api.py
6ed116053cc4a5b0e9c597880bb3ab4494ae46e92dffbe78842e39bf31ff56a9 adapters/hermes/hermes_gitops_nl_router.py
013813b58d7844f3860d6f9bb484f84f92487652b573976c474bb35c91df8ae8 adapters/openclaw/openclaw_gitops_commands.py
7a34ccdfdcdc132fe5daf01cd1acd9d9b7adbd0429d575b65e0771962ccedc62 adapters/webops/gitops_model_menu_fragment.html
86b0cb6ee3192565b080672e5305dabba1283486442e630c71244ebac4910916 config/digital_life.yaml
a3872f24e4cd64288758846ecd0b374db4aa41111feec3b17c41438136d6d5d0 config/domains.env
7a6f82cbddfc5c74bd37229e4f37fe4e73430ae585b4846abd1a80d852f2fed0 config/gitops_short_links.yaml
4139d915bdd733171734d5d5d3411e6fc4f8f202c49af4321d706da176da690b config/model_registry.yaml
8d6202d3da60366c96a208f5e5eed1f09fd36d122f4cabdc038c21ce2e72ea16 config/npm_custom_locations.yaml
868b34f7986871f6ea1c3d777751a53ed33c84160c436626bceecac73a08e61a config/roles.yaml
18b67b3ea729a37f79265cf5782760f5fb3c575218faf0463835a885f785cb38 config/runtime.yaml
4743d3fd04132e756e9493fefbaebcc532be7e8ece3c284f5cf9de8d4c536a06 config/runtime_selector.yaml
b17620786dc1a412a8285e5c28a6056eb74fd0b5fafcb625a8bb56cf8830d4c4 config/uat_cases.yaml
06ea7f9153552e92cb631661d775926e08a27b7f6cbb4e57cbc4484b057afd01 config/webops_pages.yaml
d06b69e0fe2311caaad19d63a6c497ef5447dfff14c3c0271475ea771e3fbedc delta-fix/apply_delta_fix_only.sh
79d435b2915f73fed5047c3ee3973a6b11f6829d88687a541286c40ad4166491 docs/00_PACKAGE_SCOPE_CN.md
55afed17d6f67ae9d6eac88b2522a7e7065dfedc1f59f2db7f2f637d06645f61 docs/01_ARCHITECTURE_FULL_REFRESH_CN.md
69aea6c069594bf41d715ff5dcf247fdeab7c6bca3a216abbf0a5e0a812e1038 docs/01_ARCHITECTURE_V5_DIGITAL_LIFE_CN.md
158c6cb2b14b6af9d63d23706eef28439d2d02cd6b0d28ed0dc603de8ed1915b docs/02_USER_REQUIREMENTS_OWNER_ADMIN_USER_CN.md
1989c0c6228d1db3f1d9c95de8587ae08f9dcea688f0d0c6056c72e5ab45fc36 docs/03_CODEX_OPENCLAW_DEPLOYMENT_CN.md
5b9a11da9ccdb2410c8e648923fb713f918887579a7cb51d76100cc3d3f02015 docs/03_CODEX_OPENCLAW_V5_DEPLOYMENT_CN.md
d8062c0477f16396097b6c75b3b96da1c1ee70dadd4df9568abee6122369c95a docs/04_DELTA_FIX_ONLY_RUNBOOK_CN.md
6a184451ee5e01142ab585d0f4a2aec778bb9bb0dae7e9d95a3fd3692dbcbb9c docs/04_WEBOPS_UI_V5_FULL_MATRIX_CN.md
914e43000d3354b66c97e8db44812f4f6f156d78fe287208613dba44c4e51533 docs/05_WEBOPS_UI_FUNCTION_MATRIX_CN.md
18663aeba961ac3ce300032d284e99958fd117cda1aec6b9342dc0b0cd9cd6c7 docs/06_ACCEPTANCE_UAT_V5_CN.md
c996535b9f1291e05d5e278b61b221cd4383c123aebbc1222df22a48076fe063 docs/06_UAT_ACCEPTANCE_FULL_CN.md
744f10503feccb553e4691ebc75c6a5e229e4e8eff921dd6c7f46b0cb0343710 docs/07_CHATGPT_CODEX_COLLABORATION_CN.md
2306c6be8fce7841b3244c99dba7deba37777808559679fbee1e2c1340efa18a docs/07_SELF_OPTIMIZATION_AND_ROLLBACK_CN.md
bbea36a38447a7122a095601cbb755a17e78964e548e111ba58698cd02d7fe79 docs/08_DOMAIN_ROUTE_PLAN_CN.md
56de0cd84602e9a6a31cb35d3cefddd5e228a565653e8774e15f2d0b10969b28 docs/09_BASELINE_PRESERVATION_CN.md
ddb6afc02a5dd3eb6c2caeb1a0e303f5871af24da49f337f136c79daba6ddc3c docs/10_ARCHITECTURE_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_CN.md
a061bbda9174a118c1b3bbfd9a923822cd59ccd1064d5a1269d9867a025a513b docs/11_USER_REQUIREMENTS_V1R1C0_CN.md
48250350a3c3c8fa041f1846d022ee044265aa7502b2d5fd8b65c0a5d0c7d423 docs/12_INTERFACE_API_SPEC_V1R1C0_CN.md
1abab047f7eb498c5210fd4036168c8d0f87a360db34018aee6d8ab9e8d894cb docs/13_CODEX_EXECUTION_FULL_V1R1C0_CN.md
fceb3e9dd8c89aa2909b063fb3ee257cb2a2580c2c734c12c8479f9d15a17428 docs/14_TEST_PLAN_UAT_V1R1C0_CN.md
a74f0024620e0bdcaa83638b2592c9bf36b2b489e264bb9dc166f7ff4e920267 docs/15_SECURITY_AND_PERMISSION_V1R1C0_CN.md
e497a51946abbcb83a759ad21b5bfdcbe585140c918f152672dbb7f52cf2185a docs/16_MODEL_ROUTING_CUSTOM_LLM_CN.md
84d5c4cf5ccd85eb1e5629788825ffb800c2b733011b1d0265dc444846228ee3 docs/17_NPM_SHORT_LINKS_CN.md
5fcb741ef3cad5281ae6524886a9b2425111e7d999773064d85b8184eacd7030 docs/18_RUNTIME_SELECTOR_MULTIUSER_CN.md
105a95d5862455878c04c10491f15b013fa059d62b16b9d53e959f64eb7b6a72 docs/19_GITOPS_BASELINE_VXRXC_CN.md
43c5c398b386ca75e2ea5e352d74b149731a5a728ed07cab11a65316674ac5a7 docs/20_DEPLOYMENT_RUNBOOK_V1R1C0_CN.md
cc5082cfd7379b4fb90531f958178b2513f2486530910046702218a03036e5e7 docs/CODEX_FULL_EXECUTION_PROMPT_V1R1C0.md
d8cea35eb6c1300d20804ef480312d26dbdc2ad3a2d26b9430c6ca4808155a95 fixes/patch_runtime_owner_admin_user.py
6a79b5958fdda447d3177561959c4f2a3b9c55ab32e16619a3d4ad3965979929 fixes/patch_runtime_v5_guards.py
0eb12e0161371464605577707c84df413b555277933f9eb4488ab38032ff699b migrations/001_webops_core_sqlite.sql
6d61da88e7d6b208e7eee7195b521dc47cb68587f91987d7a0dd1c8801ef168d migrations/002_seed_owner_admin_user.sql
a2c0c7699e1eb46cfdeabfc905cb79b71ed14fb6bcf81ecc99ec47975cf05767 migrations/004_gitops_custom_llm_runtime.sql
b77bce1ca78cede27de6594e25f6ce5bb0c4990c0953eeb79f7e2608d2515c32 package_verify_full.sh
f65b7fba5d6bdac3070ff4309830e286cd6319a80ab2e55263e46022fed2bf7f scripts/apply_migrations.py
bc0452d261def2366cdefd845e62d7177b42806f93ba78294ece29a8a369157e scripts/backup_before_fix.sh
bdb439086c86b3b23f8240cb2bef0f757a6be242d21d13a79eb1e44434efbe2e scripts/create_patch.sh
13ed8b9f9b2f615f97945da559181ec4a6d6d1897963e5a24c8a3741afa90300 scripts/export_evidence.sh
1fdeecf0315f3ab4ec461c3376128054df9ac48e2fe5a3f64a138cd7d710d44d scripts/generate_release_manifest.py
80c6c10f259245681c42932f3f5f9b9a0fbd9cadce6a3f495d82cbdbc8618c73 scripts/healthcheck.sh
3795cadfa6429a434c955913dbdcbf552181c88301a196b4b458f4f5dd206cd8 scripts/init_gitops_baseline.sh
4d54a4ed6d35bbb6e9fe2924f3cfceeea9c3eed898017788f6c5ab8041394a47 scripts/install_v1r1c0_gitops_model_overlay.sh
0064539ac1e1bd32dc8062ecc307ad2bc8520895009583a72d46868d1cf44e3c scripts/install_webops_overlay.sh
9fadafc4b4d098c006d159cb3b401eb19cf9ab9c5fa4b5cb109e946501c0b8e1 scripts/owner_wechat_notify_worker.py
e0abd8ad44d4f088108f2046fd9d7bbd4465af50f79bd3b9fcbb72a8c61bee65 scripts/package_contract_check_v1r1c0.sh
abef8df642c6d233af4a03c512876b02b326e19c924020b8dd8cc74236d18817 scripts/release_version.sh
bbdb3f9992b203ddc2a6a83241052663de4f021d0705724bd9df05acaa99b022 scripts/rollback_last_delta.sh
0814c26ecf6732cf2c24f5f414ad0128a159768cfbbeb21d557274064feb1188 scripts/rollback_version.sh
da84a5cc61c1d54a7d1e96e4b0a7186e192edf3ec12d694fc9d7608c3b1086ef scripts/run_acceptance_with_autofix.sh
0f55c3350bb15210382a934eaeda94480bb4916bd5edb08b82d78a965e3fabc9 scripts/run_full_uat.py
1adcccf655dfad5f31d770b877ebce05003fc76013bb80748cacfb58f7d0cbd0 scripts/run_v1r1c0_uat.py
d11050b67a7c497da01d41092ac09204f899f718249cf292b4aa3c898e4e8a3d scripts/run_v5_uat.py
0b590dbd8470b13ca79ca26303ced851c39ad2e219888540aab8f818e2935ff3 scripts/run_webops_local.sh
d9212d3798ed5f513ff731413882183520299e2d2cd3675bfc5e6d252ebaf00a scripts/run_wechat_behavior_simulation.py
ab4666f2b56094c8b982ed5f03815d053b9bd9a1b37886c54ca9e6fa009f4518 scripts/self_diagnose.py
65b70921eec67c7890527908e980dc63da6049a72bf47f7ed6496673b14705db scripts/self_diagnose_v5.py
ab2421e73619f569e8eecf651fe71bf8435c05e953e8fec2c8e9b5fd1a8e94ba scripts/self_optimize.py
6e06c0a0c53d3c3402df38dcfd326532bf4a360ec2651e245209747fdfc72b9c scripts/self_optimize_v5.py
5525f0f5d5af788185260ae2c14512e73bb5857862e7445974a4967c49c980d4 scripts/silent_install_v5.sh
1bc3bebd37f2112df27301d23527e150b79efbdaf0b2ed04dbb8f970f9d53686 scripts/smoke_test.sh
2fe86ebbbc88124e9e9b9d39cb441a3a450a015162573e87cf70a8693765b0ff scripts/validate_no_legacy_roles.py
4a4444b0a60221b6c128eef4514466293dba2d0863eee4aee6f9d9b16b727045 scripts/validate_roles_and_domains.py
a9bb2c30bf45c7ab897b439ec02286d2a4e8d946ca362da30bace445ad2af58a source-baseline/SOURCE_BASELINE_SUMMARY.json
37fd43063010b38ebac585cf18f42ea4fb487018729fdb6467cf9a3703cf4ef3 source-baseline/V5_SOURCE_BASELINE_SUMMARY.json
8456cf86202ef46f9ca6e61b14f1763f298755e5eb36d42051306620c49f471c source-baseline/current_actual_deployment_source.zip
8456cf86202ef46f9ca6e61b14f1763f298755e5eb36d42051306620c49f471c source-baseline/previous_current_actual_deployment_source_from_webops_delta.zip
48a9f80446dff49039b38508a70d0785f9e10ec8feb2700df925accf70685318 tests/run_acceptance_with_autofix.sh
4948d5f60be329d56bc246d32fb63fad57f0d1b944a7f5412cd935fbf8e7ae54 tests/run_v1r1c0_acceptance.sh
3c4675e5e202f1fc4377b662776b543fc312bd5a07cb4e8f2001f5a9f6eeceda tests/run_v5_acceptance_with_autofix.sh
f81179a2c940a568a5e5d68f1458f065e92c5636849c37d619e469b696462954 tests/test_custom_llm_registry.py
+34
View File
@@ -0,0 +1,34 @@
{
"package": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706",
"version": "V1R1C0",
"base_uploaded_package": "AIOS_v23_v5_digital_life_WEBOPS_PROD_FULL_DEPLOY_20260706(1).zip",
"generated_at": "2026-07-06T06:30:48Z",
"change_level": "R",
"features": [
"MultiUser shared/dedicated runtime selector",
"GitOps online baseline and VxRxCx versioning",
"User custom LLM registry",
"User-defined model priority and fallback",
"NPM short-link integration",
"OpenClaw and Hermes adapters",
"Full API contract and acceptance tests"
],
"entry": "https://cnnetmkt.top",
"short_links": [
"/git",
"/base",
"/ver",
"/diff",
"/patch",
"/model",
"/run",
"/api",
"/hook"
],
"main_docs": [
"docs/10_ARCHITECTURE_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_CN.md",
"docs/11_USER_REQUIREMENTS_V1R1C0_CN.md",
"docs/12_INTERFACE_API_SPEC_V1R1C0_CN.md",
"docs/CODEX_FULL_EXECUTION_PROMPT_V1R1C0.md"
]
}
+29
View File
@@ -0,0 +1,29 @@
# AIOS V1R1C0 MultiUser GitOps Custom LLM 生产工程部署包
本包基于 `AIOS_v23_v5_digital_life_WEBOPS_PROD_FULL_DEPLOY_20260706` 刷新,版本号升级为:`V1R1C0`。
## 本版新增能力
1. 借鉴 MultiUserClaw 的 Platform Gateway / shared + dedicated runtime 思路,但复用现有 AIOS WebOps、NPM、Casdoor、OpenClaw、Hermes。
2. 建立 GitOps 在线基线机制,后续不再定期上传 ZIP,通过 Git branch / patch / tag / diff 刷新。
3. 支持 `VxRxCx` 版本体系:V=架构,R=功能,C=修复。
4. 支持用户在基础模型之外添加自定义 LLM 模型,并自主设置模型优先级与 fallback 顺序。
5. 新增 GitOps + Model Registry + Runtime Selector API 适配器,默认监听 `127.0.0.1:18890`。
6. 保留短链接:`/git /base /ver /diff /patch /run /api /model /hook`。
7. 通过 NPM 暴露入口,避免新增裸 Nginx 配置。
## 推荐执行
```bash
cd <解压目录>
bash scripts/install_v1r1c0_gitops_model_overlay.sh
bash tests/run_v1r1c0_acceptance.sh
```
## 给 Codex 的完整执行文档
```text
docs/CODEX_FULL_EXECUTION_PROMPT_V1R1C0.md
```
本包是 V1R1C0 全量工程部署包,包含文档、脚本、迁移、接口适配器、测试用例。部署时应由 Codex 在服务器上定位当前生产包和运行目录,按增量方式叠加,不得破坏现有 WebOps/NPM/Casdoor/OpenClaw/Hermes。
+1
View File
@@ -0,0 +1 @@
V1R1C0
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env python3
from __future__ import annotations
import json, os, re, sqlite3, subprocess, time, uuid, hashlib, urllib.parse, html
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
WORKSPACE = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
DB_PATH = Path(os.getenv('AIOS_GITOPS_DB', str(ROOT / 'runtime' / 'gitops_model.sqlite3')))
SECRETS_DIR = Path(os.getenv('AIOS_GITOPS_SECRETS_DIR', str(ROOT / 'runtime' / 'secrets')))
TOKEN_FILE = Path(os.getenv('AIOS_GITOPS_TOKEN_FILE', str(SECRETS_DIR / 'gitops.tokens')))
HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
ENTRY = os.getenv('AIOS_ENTRY', 'https://cnnetmkt.top')
DEV_ALLOW = os.getenv('AIOS_GITOPS_DEV_ALLOW', '0') == '1'
BASE_MODELS = [
{'id':'base.deepseekv4-flash','provider':'one-api','model':'deepseekv4-flash','display':'DeepSeek V4 Flash','tier':'regular','reasoning':1,'vision':0,'tools':1,'context':128000},
{'id':'base.qwen3.6-35b','provider':'one-api','model':'qwen3.6-35b','display':'Qwen 3.6 35B','tier':'pro','reasoning':1,'vision':0,'tools':1,'context':262144},
{'id':'base.gpt-5.5','provider':'openai','model':'gpt-5.5','display':'GPT-5.5','tier':'ultra','reasoning':1,'vision':1,'tools':1,'context':256000},
]
def now() -> str: return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
def uid(prefix='id') -> str: return f'{prefix}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
def esc(x: Any) -> str: return html.escape('' if x is None else str(x))
def safe_name(s: str) -> str: return re.sub(r'[^a-zA-Z0-9_.@-]+', '_', s or 'unknown')[:120]
def db():
DB_PATH.parent.mkdir(parents=True, exist_ok=True); con = sqlite3.connect(DB_PATH); con.row_factory = sqlite3.Row; return con
def rows(sql, args=()):
with db() as con: return [dict(r) for r in con.execute(sql, args).fetchall()]
def one(sql, args=()):
r = rows(sql, args); return r[0] if r else None
def exec_sql(sql, args=()):
with db() as con: con.execute(sql, args); con.commit()
def audit(actor, action, target='', payload=None): exec_sql('INSERT INTO model_audit_logs(id,actor,action,target,payload,created_at) VALUES(?,?,?,?,?,?)',(uid('audit'), actor, action, target, json.dumps(payload or {}, ensure_ascii=False), now()))
def init_db():
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
schema = Path(__file__).resolve().parents[1] / 'migrations' / '004_gitops_custom_llm_runtime.sql'
with db() as con:
if schema.exists(): con.executescript(schema.read_text(encoding='utf-8'))
else:
con.executescript('''CREATE TABLE IF NOT EXISTS gitops_baseline(id TEXT PRIMARY KEY,active_version TEXT,active_tag TEXT,source_package TEXT,entry TEXT,update_mode TEXT,status TEXT,created_at TEXT,updated_at TEXT);CREATE TABLE IF NOT EXISTS gitops_versions(version TEXT PRIMARY KEY,tag TEXT,level TEXT,summary TEXT,status TEXT,created_by TEXT,created_at TEXT);CREATE TABLE IF NOT EXISTS gitops_patches(id TEXT PRIMARY KEY,patch_name TEXT,branch TEXT,base_ref TEXT,target_version TEXT,risk TEXT,status TEXT,created_by TEXT,created_at TEXT,applied_at TEXT,result TEXT);CREATE TABLE IF NOT EXISTS llm_providers(id TEXT PRIMARY KEY,owner_username TEXT,name TEXT,provider_type TEXT,base_url TEXT,auth_mode TEXT,api_key_secret_ref TEXT,scope TEXT,status TEXT,created_at TEXT,updated_at TEXT);CREATE TABLE IF NOT EXISTS llm_models(id TEXT PRIMARY KEY,provider_id TEXT,owner_username TEXT,model_name TEXT,display_name TEXT,context_window INTEGER,input_modalities TEXT,output_modalities TEXT,supports_tools INTEGER,supports_vision INTEGER,supports_reasoning INTEGER,cost_tier TEXT,is_base INTEGER,scope TEXT,status TEXT,created_at TEXT,updated_at TEXT, UNIQUE(owner_username, provider_id, model_name));CREATE TABLE IF NOT EXISTS user_model_priority(username TEXT,model_id TEXT,priority INTEGER,enabled INTEGER,fallback_policy TEXT,updated_at TEXT,PRIMARY KEY(username,model_id));CREATE TABLE IF NOT EXISTS model_route_decisions(id TEXT PRIMARY KEY,username TEXT,task_type TEXT,selected_model_id TEXT,candidate_order TEXT,reason TEXT,created_at TEXT);CREATE TABLE IF NOT EXISTS user_runtime_profiles(username TEXT PRIMARY KEY,role TEXT,tier TEXT,runtime_mode TEXT,allow_dedicated INTEGER,allow_custom_llm INTEGER,updated_at TEXT);CREATE TABLE IF NOT EXISTS runtime_instances(id TEXT PRIMARY KEY,username TEXT,runtime_type TEXT,backend TEXT,status TEXT,endpoint TEXT,last_seen_at TEXT,created_at TEXT,metadata TEXT);CREATE TABLE IF NOT EXISTS model_audit_logs(id TEXT PRIMARY KEY,actor TEXT,action TEXT,target TEXT,payload TEXT,created_at TEXT);''')
con.commit()
seed_base_models()
def seed_base_models():
exec_sql("INSERT OR IGNORE INTO llm_providers(id,owner_username,name,provider_type,base_url,auth_mode,api_key_secret_ref,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)", ('provider.one-api','system','AIOS One API','openai_compatible','http://127.0.0.1:13000/v1','bearer','secret://one-api/global','global','active',now(),now()))
exec_sql("INSERT OR IGNORE INTO llm_providers(id,owner_username,name,provider_type,base_url,auth_mode,api_key_secret_ref,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)", ('provider.openai','system','OpenAI Official','openai_compatible','https://api.openai.com/v1','bearer','secret://openai/global','global','active',now(),now()))
for m in BASE_MODELS:
pid = 'provider.openai' if m['provider']=='openai' else 'provider.one-api'
exec_sql('''INSERT OR IGNORE INTO llm_models(id,provider_id,owner_username,model_name,display_name,context_window,input_modalities,output_modalities,supports_tools,supports_vision,supports_reasoning,cost_tier,is_base,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', (m['id'], pid, 'system', m['model'], m['display'], m['context'], '["text"]', '["text"]', m['tools'], m['vision'], m['reasoning'], m['tier'], 1, 'global', 'active', now(), now()))
if not one('SELECT * FROM gitops_baseline WHERE id=?', ('current',)):
exec_sql('INSERT INTO gitops_baseline(id,active_version,active_tag,source_package,entry,update_mode,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)', ('current','V1R1C0','V1R1C0','AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706',ENTRY,'git_patch_only','active',now(),now()))
if not one('SELECT * FROM gitops_versions WHERE version=?', ('V1R1C0',)):
exec_sql('INSERT INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)', ('V1R1C0','V1R1C0','R','MultiUser runtime selector + GitOps + custom LLM priority registry','ready','system',now()))
def valid_token(token: str) -> bool:
if DEV_ALLOW: return True
if not token or not TOKEN_FILE.exists(): return False
return token.strip() in [x.strip() for x in TOKEN_FILE.read_text().splitlines() if x.strip()]
def git_cmd(args, check=False):
WORKSPACE.mkdir(parents=True, exist_ok=True); p = subprocess.run(['git'] + args, cwd=str(WORKSPACE), text=True, capture_output=True)
if check and p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or 'git command failed')
return {'code': p.returncode, 'stdout': p.stdout.strip(), 'stderr': p.stderr.strip(), 'cmd': ['git']+args}
def model_visible_to(username: str): return rows('''SELECT m.*, p.name provider_name, p.base_url, p.provider_type, p.scope provider_scope FROM llm_models m JOIN llm_providers p ON p.id=m.provider_id WHERE m.status='active' AND (m.scope='global' OR m.owner_username=? OR p.owner_username=?) ORDER BY m.is_base DESC, m.created_at ASC''', (username, username))
def priority_for(username: str): return rows('''SELECT pr.username, pr.model_id, pr.priority, pr.enabled, pr.fallback_policy, pr.updated_at, m.model_name, m.display_name, m.is_base, m.owner_username, p.base_url, p.name provider_name FROM user_model_priority pr JOIN llm_models m ON m.id=pr.model_id JOIN llm_providers p ON p.id=m.provider_id WHERE pr.username=? AND m.status='active' ORDER BY pr.priority ASC''', (username,))
def route_model(username: str, task_type='chat'):
ordered = [r for r in priority_for(username) if int(r.get('enabled') or 0) == 1]
if not ordered:
ordered = [m for m in model_visible_to(username) if int(m.get('is_base') or 0) == 1]
selected = dict(ordered[0]) if ordered else None
if selected and 'id' not in selected and selected.get('model_id'):
selected['id'] = selected.get('model_id')
reason = 'user_priority' if priority_for(username) else 'role_default_base_model'
candidate_ids = [(x.get('id') or x.get('model_id')) for x in ordered]
selected_id = selected.get('id') if selected else None
exec_sql('INSERT INTO model_route_decisions(id,username,task_type,selected_model_id,candidate_order,reason,created_at) VALUES(?,?,?,?,?,?,?)',(uid('route'), username, task_type, selected_id, json.dumps(candidate_ids, ensure_ascii=False), reason, now()))
return {'username': username, 'task_type': task_type, 'selected': selected, 'candidate_order': ordered, 'reason': reason}
def parse_body(handler):
n = int(handler.headers.get('Content-Length','0') or '0')
if n <= 0: return {}
raw = handler.rfile.read(n).decode('utf-8')
try: return json.loads(raw or '{}')
except Exception: return {'_raw': raw}
class Handler(BaseHTTPRequestHandler):
server_version = 'AIOSGitOpsModelAPI/V1R1C0'
def actor(self):
auth = self.headers.get('Authorization',''); token = auth.replace('Bearer ', '').strip() if auth.startswith('Bearer ') else self.headers.get('X-AIOS-Token','')
ok = valid_token(token); user = self.headers.get('X-AIOS-User') or ('dev-owner' if DEV_ALLOW else 'anonymous'); role = self.headers.get('X-AIOS-Role') or ('owner' if DEV_ALLOW else 'user')
return {'username': safe_name(user), 'role': role, 'token_ok': ok}
def require_write(self, roles=None):
a = self.actor()
if not a['token_ok']: self.send_json({'error':'unauthorized','hint':'write APIs require Bearer token or AIOS_GITOPS_DEV_ALLOW=1'}, 401); return None
if roles and a['role'] not in roles: self.send_json({'error':'forbidden','required_roles':roles,'role':a['role']}, 403); return None
return a
def send_json(self, data, code=200):
b = json.dumps(data, ensure_ascii=False, indent=2).encode('utf-8'); self.send_response(code); self.send_header('Content-Type','application/json; charset=utf-8'); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
def send_html(self, title, body, code=200):
links = '<a href="/base">base</a> <a href="/ver">ver</a> <a href="/diff">diff</a> <a href="/patch">patch</a> <a href="/run">run</a> <a href="/model">model</a> <a href="/api">api</a>'
html_doc = f'''<!doctype html><html><head><meta charset="utf-8"><title>{esc(title)}</title><style>body{{font-family:Arial,"Microsoft YaHei",sans-serif;margin:24px;background:#f8fafc}}a{{margin-right:12px}}.card{{background:white;border:1px solid #e5e7eb;border-radius:12px;padding:16px;margin:12px 0}}table{{border-collapse:collapse;width:100%;background:white}}td,th{{border:1px solid #ddd;padding:6px;text-align:left;vertical-align:top}}pre{{background:#0f172a;color:#e2e8f0;padding:12px;border-radius:8px;overflow:auto}}</style></head><body><h1>{esc(title)}</h1><div class="card">{links}</div>{body}</body></html>'''
b = html_doc.encode('utf-8'); self.send_response(code); self.send_header('Content-Type','text/html; charset=utf-8'); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
def table(self, data):
if not data: return '<p>No records.</p>'
cols = list(data[0].keys()); return '<table><thead><tr>'+''.join(f'<th>{esc(c)}</th>' for c in cols)+'</tr></thead><tbody>'+''.join('<tr>'+''.join(f'<td>{esc(r.get(c))}</td>' for c in cols)+'</tr>' for r in data)+'</tbody></table>'
def do_GET(self):
init_db(); u = urllib.parse.urlparse(self.path); path = u.path; q = urllib.parse.parse_qs(u.query); username = self.actor()['username']
try:
if path in ('/health','/api/health'): self.send_json({'status':'ok','version':'V1R1C0','entry':ENTRY,'workspace':str(WORKSPACE),'db':str(DB_PATH)}); return
if path in ('/api/status','/api'):
if path == '/api': self.send_html('AIOS V1R1C0 API', '<pre>'+esc(json.dumps(api_spec(),ensure_ascii=False,indent=2))+'</pre>'); return
self.send_json({'version':'V1R1C0','git':git_cmd(['status','--short']),'baseline':one('SELECT * FROM gitops_baseline WHERE id=?',('current',))}); return
if path in ('/api/base','/base'):
data = one('SELECT * FROM gitops_baseline WHERE id=?',('current',)) or {}; self.send_html('Baseline', '<pre>'+esc(json.dumps(data,ensure_ascii=False,indent=2))+'</pre>') if path == '/base' else self.send_json(data); return
if path in ('/api/ver','/ver'):
data = rows('SELECT * FROM gitops_versions ORDER BY created_at DESC'); self.send_html('Versions', self.table(data)) if path == '/ver' else self.send_json(data); return
if path in ('/api/patch','/patch'):
data = rows('SELECT * FROM gitops_patches ORDER BY created_at DESC'); self.send_html('Patches', self.table(data)) if path == '/patch' else self.send_json(data); return
if path in ('/api/diff','/diff'):
frm = (q.get('from') or q.get('from_') or ['HEAD~1'])[0]; to = (q.get('to') or ['HEAD'])[0]; diff = git_cmd(['diff', frm, to])
self.send_html('Diff', f'<p>from={esc(frm)} to={esc(to)}</p><pre>{esc(diff["stdout"] or diff["stderr"])}</pre>') if path == '/diff' else self.send_json({'from':frm,'to':to,'diff':diff}); return
if path in ('/api/model/base','/api/model/custom','/api/model/priority','/model'):
if path == '/api/model/base': self.send_json([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==1]); return
if path == '/api/model/custom': self.send_json([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==0]); return
if path == '/api/model/priority': self.send_json(priority_for(username)); return
body = '<h2>Base Models</h2>'+self.table([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==1]); body += '<h2>Custom Models</h2>'+self.table([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==0]); body += '<h2>Priority</h2>'+self.table(priority_for(username)); self.send_html('Model Registry', body); return
if path in ('/api/runtime/status','/run'):
data = rows('SELECT * FROM runtime_instances ORDER BY created_at DESC LIMIT 100'); self.send_html('Runtime', self.table(data)) if path == '/run' else self.send_json(data); return
if path == '/api/audit': self.send_json(rows('SELECT * FROM model_audit_logs ORDER BY created_at DESC LIMIT 200')); return
self.send_json({'error':'not_found','path':path},404)
except Exception as e: self.send_json({'error':str(e)},500)
def do_POST(self):
init_db(); path = urllib.parse.urlparse(self.path).path; data = parse_body(self)
try:
if path == '/api/model/custom/add':
a = self.require_write();
if not a: return
username = safe_name(data.get('username') or a['username']); provider_name = data.get('provider_name') or data.get('name') or 'custom-provider'; base_url = (data.get('base_url') or '').rstrip('/'); model_name = data.get('model_name') or data.get('model')
if not base_url or not model_name: self.send_json({'error':'base_url and model_name required'},400); return
secret_ref = data.get('api_key_secret_ref')
if data.get('api_key'):
sdir = SECRETS_DIR / 'llm' / safe_name(username); sdir.mkdir(parents=True, exist_ok=True); secret_id = uid('llmkey'); sp = sdir / f'{secret_id}.secret'; sp.write_text(str(data['api_key']), encoding='utf-8'); os.chmod(sp, 0o600); secret_ref = f'secret://llm/{safe_name(username)}/{secret_id}'
provider_id = data.get('provider_id') or f'provider.custom.{safe_name(username)}.{hashlib.sha1((provider_name+base_url).encode()).hexdigest()[:8]}'; model_id = data.get('model_id') or f'custom.{safe_name(username)}.{hashlib.sha1((provider_id+model_name).encode()).hexdigest()[:10]}'
exec_sql('''INSERT OR REPLACE INTO llm_providers(id,owner_username,name,provider_type,base_url,auth_mode,api_key_secret_ref,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)''',(provider_id, username, provider_name, data.get('provider_type','openai_compatible'), base_url, data.get('auth_mode','bearer'), secret_ref, data.get('scope','user'), 'active', now(), now()))
exec_sql('''INSERT OR REPLACE INTO llm_models(id,provider_id,owner_username,model_name,display_name,context_window,input_modalities,output_modalities,supports_tools,supports_vision,supports_reasoning,cost_tier,is_base,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',(model_id, provider_id, username, model_name, data.get('display_name') or model_name, int(data.get('context_window') or 0), json.dumps(data.get('input_modalities') or ['text'],ensure_ascii=False), json.dumps(data.get('output_modalities') or ['text'],ensure_ascii=False), int(bool(data.get('supports_tools',1))), int(bool(data.get('supports_vision',0))), int(bool(data.get('supports_reasoning',0))), data.get('cost_tier','custom'), 0, data.get('scope','user'), 'active', now(), now()))
if 'priority' in data: exec_sql('INSERT OR REPLACE INTO user_model_priority(username,model_id,priority,enabled,fallback_policy,updated_at) VALUES(?,?,?,?,?,?)',(username, model_id, int(data.get('priority')), int(bool(data.get('enabled',1))), data.get('fallback_policy','try_next_on_error'), now()))
audit(a['username'],'custom_model_add',model_id,{'username':username,'provider_id':provider_id,'model_name':model_name}); self.send_json({'status':'ok','provider_id':provider_id,'model_id':model_id,'secret_ref':secret_ref}); return
if path == '/api/model/custom/update':
a = self.require_write();
if not a: return
model_id = data.get('model_id'); m = one('SELECT * FROM llm_models WHERE id=?',(model_id,))
if not m: self.send_json({'error':'model not found'},404); return
if m['owner_username'] != a['username'] and a['role'] not in ('owner','admin'): self.send_json({'error':'forbidden'},403); return
fields=[]; args=[]
for col in ['display_name','status','cost_tier']:
if col in data: fields.append(f'{col}=?'); args.append(data[col])
for col in ['context_window','supports_tools','supports_vision','supports_reasoning']:
if col in data: fields.append(f'{col}=?'); args.append(int(bool(data[col])) if col.startswith('supports') else int(data[col]))
fields.append('updated_at=?'); args.append(now()); args.append(model_id); exec_sql(f'UPDATE llm_models SET {", ".join(fields)} WHERE id=?', args); audit(a['username'],'custom_model_update',model_id,data); self.send_json({'status':'updated','model_id':model_id}); return
if path == '/api/model/delete':
a = self.require_write();
if not a: return
model_id = data.get('model_id'); m = one('SELECT * FROM llm_models WHERE id=?',(model_id,))
if not m: self.send_json({'error':'model not found'},404); return
if int(m.get('is_base') or 0)==1: self.send_json({'error':'base model cannot be deleted'},400); return
if m['owner_username'] != a['username'] and a['role'] not in ('owner','admin'): self.send_json({'error':'forbidden'},403); return
exec_sql('UPDATE llm_models SET status=?,updated_at=? WHERE id=?',('disabled',now(),model_id)); audit(a['username'],'custom_model_delete',model_id,{}); self.send_json({'status':'disabled','model_id':model_id}); return
if path == '/api/model/priority/set':
a = self.require_write();
if not a: return
username = safe_name(data.get('username') or a['username'])
if username != a['username'] and a['role'] not in ('owner','admin'): self.send_json({'error':'forbidden'},403); return
items = data.get('items') or data.get('priority') or []; exec_sql('DELETE FROM user_model_priority WHERE username=?',(username,)); visible={m['id'] for m in model_visible_to(username)}
for idx,item in enumerate(items):
if isinstance(item,str): item={'model_id':item,'priority':(idx+1)*10,'enabled':1}
mid=item.get('model_id') or item.get('id')
if mid and mid in visible: exec_sql('INSERT OR REPLACE INTO user_model_priority(username,model_id,priority,enabled,fallback_policy,updated_at) VALUES(?,?,?,?,?,?)',(username, mid, int(item.get('priority',(idx+1)*10)), int(bool(item.get('enabled',1))), item.get('fallback_policy','try_next_on_error'), now()))
audit(a['username'],'model_priority_set',username,{'items':items}); self.send_json({'status':'updated','username':username,'priority':priority_for(username)}); return
if path == '/api/model/route/resolve': a=self.actor(); self.send_json(route_model(safe_name(data.get('username') or a['username']), data.get('task_type','chat'))); return
if path == '/api/model/test':
a = self.require_write();
if not a: return
mid=data.get('model_id'); m=one('SELECT m.*,p.base_url,p.provider_type FROM llm_models m JOIN llm_providers p ON p.id=m.provider_id WHERE m.id=?',(mid,))
if not m: self.send_json({'error':'model not found'},404); return
self.send_json({'status':'ok' if m['base_url'] and m['model_name'] else 'failed','live_test':os.getenv('AIOS_ENABLE_MODEL_LIVE_TEST','0')=='1','model':m['model_name'],'base_url':m['base_url'],'note':'Network live call is disabled unless AIOS_ENABLE_MODEL_LIVE_TEST=1'}); return
if path == '/api/runtime/select':
a=self.actor(); username=safe_name(data.get('username') or a['username']); role=data.get('role') or a['role']; tier=data.get('tier') or ('ultra' if role=='owner' else 'regular'); risk=data.get('risk','normal')
if risk in ('high_tool','shell','docker','write_prod'): runtime,backend='ephemeral_sandbox','openclaw_executor'
elif role in ('owner','admin') or tier=='ultra': runtime,backend='dedicated','hermes'
elif tier=='pro': runtime,backend='dedicated_or_shared','hermes'
else: runtime,backend='shared','hermes'
exec_sql('INSERT OR REPLACE INTO user_runtime_profiles(username,role,tier,runtime_mode,allow_dedicated,allow_custom_llm,updated_at) VALUES(?,?,?,?,?,?,?)',(username,role,tier,runtime,1 if runtime!='shared' else 0,1,now())); self.send_json({'username':username,'role':role,'tier':tier,'runtime_type':runtime,'backend':backend,'reason':'V1R1C0 selector policy'}); return
if path == '/api/runtime/register':
a = self.require_write(['owner','admin']);
if not a: return
rid=data.get('id') or uid('rt'); exec_sql('INSERT OR REPLACE INTO runtime_instances(id,username,runtime_type,backend,status,endpoint,last_seen_at,created_at,metadata) VALUES(?,?,?,?,?,?,?,?,?)',(rid,safe_name(data.get('username') or 'shared'),data.get('runtime_type','shared'),data.get('backend','hermes'),data.get('status','registered'),data.get('endpoint'),now(),now(),json.dumps(data.get('metadata') or {},ensure_ascii=False))); audit(a['username'],'runtime_register',rid,data); self.send_json({'status':'registered','id':rid}); return
if path == '/api/runtime/heartbeat': exec_sql('UPDATE runtime_instances SET status=?,last_seen_at=?,metadata=? WHERE id=?',(data.get('status','ok'),now(),json.dumps(data.get('metadata') or {},ensure_ascii=False),data.get('id'))); self.send_json({'status':'ok','id':data.get('id')}); return
if path == '/api/branch':
a = self.require_write(['owner','admin']);
if not a: return
branch=data.get('branch'); base=data.get('base','main')
if not branch: self.send_json({'error':'branch required'},400); return
git_cmd(['fetch','--all']); git_cmd(['checkout',base]); git_cmd(['checkout','-B',branch],check=True); audit(a['username'],'git_branch',branch,data); self.send_json({'status':'ok','branch':branch}); return
if path == '/api/file':
a = self.require_write(['owner','admin']);
if not a: return
branch=data.get('branch') or 'main'; rel=data.get('path'); content=data.get('content',''); msg=data.get('message') or f'chore(gitops): update {rel}'
if not rel or rel.startswith('/') or '..' in Path(rel).parts: self.send_json({'error':'invalid path'},400); return
git_cmd(['checkout',branch],check=True); target=WORKSPACE/rel; target.parent.mkdir(parents=True,exist_ok=True); target.write_text(content,encoding='utf-8'); git_cmd(['add',rel],check=True); commit=git_cmd(['commit','-m',msg]); audit(a['username'],'git_file_update',rel,{'branch':branch,'commit':commit}); self.send_json({'status':'committed','path':rel,'commit':commit}); return
if path == '/api/commit':
a = self.require_write(['owner','admin']);
if not a: return
msg=data.get('message') or 'chore(gitops): commit changes'; git_cmd(['add','.'],check=True); out=git_cmd(['commit','-m',msg]); audit(a['username'],'git_commit','workspace',{'message':msg}); self.send_json({'status':'ok','commit':out}); return
if path == '/api/tag':
a = self.require_write(['owner']);
if not a: return
ver=data.get('version') or data.get('tag')
if not re.match(r'^V\d+R\d+C\d+(-[a-zA-Z0-9_.-]+)?$', ver or ''): self.send_json({'error':'version must match VxRxCx'},400); return
msg=data.get('message') or f'release {ver}'; out=git_cmd(['tag','-a',ver,'-m',msg]);
if data.get('push',False): git_cmd(['push','origin',ver])
level='V' if re.match(r'^V[1-9]\d*R0C0', ver) else ('R' if re.match(r'^V\d+R[1-9]\d*C0', ver) else 'C'); exec_sql('INSERT OR REPLACE INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(ver,ver,level,msg,'ready',a['username'],now())); exec_sql('UPDATE gitops_baseline SET active_version=?,active_tag=?,updated_at=? WHERE id=?',(ver,ver,now(),'current')); audit(a['username'],'git_tag',ver,{'level':level}); self.send_json({'status':'tagged','version':ver,'git':out}); return
if path == '/api/rollback':
a = self.require_write(['owner','admin']);
if not a: return
target=data.get('target_version') or data.get('version')
if not target: self.send_json({'error':'target_version required'},400); return
out=git_cmd(['checkout',target]); audit(a['username'],'git_rollback',target,{}); self.send_json({'status':'rollback_checkout','target':target,'git':out}); return
if path == '/api/nl':
a = self.require_write();
if not a: return
text=data.get('text',''); intent='unknown'; plan=[]
if '回滚' in text or 'rollback' in text.lower(): intent='rollback'; plan=['extract target VxRxCx','call /api/rollback','run smoke test']
elif '对比' in text or 'diff' in text.lower(): intent='diff'; plan=['extract from/to refs','call /api/diff']
elif '模型' in text and ('优先' in text or 'priority' in text.lower()): intent='set_model_priority'; plan=['resolve model ids','call /api/model/priority/set']
elif '模型' in text and ('添加' in text or '新增' in text or 'add' in text.lower()): intent='add_custom_model'; plan=['validate provider','store api key as secret_ref','call /api/model/custom/add']
elif 'patch' in text.lower() or '补丁' in text: intent='create_patch'; plan=['create patch branch','commit patch metadata','run tests']
self.send_json({'intent':intent,'action_plan':plan,'execute':False,'safety_check':'V-level changes require owner approval'}); return
self.send_json({'error':'not_found','path':path},404)
except Exception as e: self.send_json({'error':str(e)},500)
def api_spec(): return {'version':'V1R1C0','short_links':['/base','/ver','/diff','/patch','/run','/model','/api'],'gitops_get':['/api/status','/api/base','/api/ver','/api/diff','/api/patch'],'gitops_post':['/api/branch','/api/file','/api/commit','/api/tag','/api/rollback','/api/nl'],'model_get':['/api/model/base','/api/model/custom','/api/model/priority'],'model_post':['/api/model/custom/add','/api/model/custom/update','/api/model/delete','/api/model/priority/set','/api/model/route/resolve','/api/model/test'],'runtime':['/api/runtime/status','/api/runtime/select','/api/runtime/register','/api/runtime/heartbeat'],'auth':'write APIs require Bearer token unless AIOS_GITOPS_DEV_ALLOW=1 for local acceptance tests'}
def main(): init_db(); print(f'AIOS V1R1C0 GitOps Model API listening on {HOST}:{PORT}'); ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
if __name__ == '__main__': main()
+282
View File
@@ -0,0 +1,282 @@
#!/usr/bin/env python3
from __future__ import annotations
import hashlib, html, json, os, sqlite3, time, uuid, urllib.parse
from http import cookies
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
ROOT = Path(os.getenv('AIOS_WEBOPS_ROOT', Path(__file__).resolve().parents[1]))
DB_PATH = Path(os.getenv('AIOS_WEBOPS_DB', ROOT / 'data' / 'webops.sqlite3'))
HOST = os.getenv('AIOS_WEBOPS_HOST','0.0.0.0')
PORT = int(os.getenv('AIOS_WEBOPS_PORT','18081'))
PUBLIC_DOMAIN = os.getenv('AIOS_PUBLIC_DOMAIN','https://cnnetmkt.top')
WEBOPS_DOMAIN = os.getenv('AIOS_WEBOPS_DOMAIN','https://ops.cnnetmkt.top')
API_DOMAIN = os.getenv('AIOS_API_DOMAIN','https://api.cnnetmkt.top')
WECHAT_DOMAIN = os.getenv('AIOS_WECHAT_CALLBACK_DOMAIN','https://wechat.cnnetmkt.top')
SECRET = os.getenv('AIOS_WEBOPS_SECRET','change-me-before-production')
ROLES = ['owner','admin','user']
NODES = ['server-hw','server-local']
PAGES = [('/dashboard','Dashboard'),('/nodes','Nodes'),('/roles','Roles'),('/routing','Routing'),('/failover','Failover'),('/approvals','Approvals'),('/operations','Operations'),('/users','Users'),('/quota','Quota'),('/models','Models'),('/memory','Memory'),('/channels/wechat','WeChat'),('/life','Life Core'),('/patches','Patch Studio'),('/optimization','Optimization'),('/simulation','Simulation'),('/logs','Logs'),('/config','Config'),('/uat','UAT'),('/versions','Versions'),('/evidence','Evidence')]
def now(): return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
def uid(p='id'): return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
def esc(x): return html.escape('' if x is None else str(x))
def jdump(x): return json.dumps(x, ensure_ascii=False)
def jload(x):
try: return json.loads(x or '{}')
except Exception: return {}
def db():
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
con=sqlite3.connect(DB_PATH); con.row_factory=sqlite3.Row; return con
def exec_sql(sql,args=()):
with db() as con: con.execute(sql,args); con.commit()
def rows(sql,args=()):
with db() as con: return [dict(r) for r in con.execute(sql,args).fetchall()]
def one(sql,args=()):
r=rows(sql,args); return r[0] if r else None
def init_db():
(ROOT/'data').mkdir(parents=True,exist_ok=True)
for f in sorted((ROOT/'migrations').glob('*.sql')):
with db() as con:
con.executescript(f.read_text(encoding='utf-8')); con.commit()
def audit(actor, action, target='', payload=None):
exec_sql('INSERT INTO audit_logs(id,actor,action,target,payload,created_at) VALUES(?,?,?,?,?,?)',(uid('audit'),actor,action,target,jdump(payload or {}),now()))
def notify(event_type,payload):
exec_sql('INSERT INTO owner_notifications(id,event_type,payload,status,created_at) VALUES(?,?,?,?,?)',(uid('notify'),event_type,jdump(payload),'queued',now()))
def hash_pw(p): return hashlib.sha256((SECRET+'|'+p).encode()).hexdigest()
def check_pw(stored,p): return stored in (p, hash_pw(p))
def token(username,role):
data=f'{username}|{role}|{int(time.time())}'; sig=hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]; return data+'|'+sig
def parse_session(header):
if not header: return None
c=cookies.SimpleCookie(); c.load(header)
if 'aios_session' not in c: return None
parts=c['aios_session'].value.split('|')
if len(parts)!=4: return None
data='|'.join(parts[:3]); sig=parts[3]
if hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]!=sig: return None
return one('SELECT * FROM users WHERE username=? AND role=? AND status=?',(parts[0],parts[1],'approved'))
def require(user, roles): return bool(user and user['role'] in roles)
def table(data):
if not data: return '<p class="muted">No records.</p>'
cols=list(data[0].keys()); head=''.join(f'<th>{esc(c)}</th>' for c in cols)
body=''.join('<tr>'+''.join(f'<td>{esc(r.get(c))}</td>' for c in cols)+'</tr>' for r in data)
return f'<table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>'
def nav(user):
links=''.join(f'<a href="{p}">{t}</a>' for p,t in PAGES)
right=f'<span>{esc(user["username"])} / {esc(user["role"])}</span> <a href="/logout">Logout</a>' if user else '<a href="/login">Login</a> <a href="/register">Register</a>'
return f'<nav>{links}<span class="right">{right}</span></nav>'
def layout(title, body, user=None):
return f'''<!doctype html><html><head><meta charset="utf-8"><title>{esc(title)}</title><style>
body{{font-family:Arial,"Microsoft YaHei",sans-serif;margin:0;background:#f7f8fb;color:#111}}nav{{background:#111827;color:white;padding:10px;position:sticky;top:0}}nav a{{color:white;margin-right:10px;text-decoration:none}}.right{{float:right}}.wrap{{padding:18px}}.card{{background:white;border-radius:12px;padding:16px;margin:12px 0;box-shadow:0 1px 5px #ddd}}table{{border-collapse:collapse;width:100%}}th,td{{border:1px solid #e5e7eb;padding:7px;text-align:left;vertical-align:top}}th{{background:#f3f4f6}}button{{background:#2563eb;color:white;border:0;padding:8px 12px;border-radius:8px;cursor:pointer;margin:3px}}button.danger{{background:#dc2626}}button.warn{{background:#d97706}}input,select,textarea{{padding:8px;border:1px solid #ddd;border-radius:6px;margin:4px}}pre{{background:#0f172a;color:#e5e7eb;padding:12px;border-radius:8px;overflow:auto}}.grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px}}.muted{{color:#6b7280}}</style><script>
async function post(path,data){{let r=await fetch(path,{{method:'POST',headers:{{'Content-Type':'application/json'}},body:JSON.stringify(data||{{}})}}); let t=await r.text(); try{{alert(JSON.stringify(JSON.parse(t),null,2));}}catch(e){{alert(t)}} location.reload();}}
</script></head><body>{nav(user)}<div class="wrap"><h1>{esc(title)}</h1>{body}</div></body></html>'''
def approval(actor, action, payload, risk='medium'):
aid=uid('approval'); exec_sql('INSERT INTO approvals(id,action,payload,requested_by,status,risk,created_at) VALUES(?,?,?,?,?,?,?)',(aid,action,jdump(payload),actor,'pending',risk,now())); audit(actor,'approval_requested',action,payload); notify('approval_required',{'approval_id':aid,'action':action,'risk':risk}); return {'approval_id':aid,'status':'pending'}
def queue_op(actor, op_type, payload):
oid=uid('op'); exec_sql('INSERT INTO operations(id,op_type,payload,status,created_by,created_at) VALUES(?,?,?,?,?,?)',(oid,op_type,jdump(payload),'queued',actor,now())); audit(actor,'operation_queued',op_type,{'operation_id':oid,**payload}); notify('operation_queued',{'operation_id':oid,'op_type':op_type}); return {'operation_id':oid,'status':'queued'}
def simulate_wechat(username, message_type, content):
if message_type in ('image','quote_image'):
route='vision'; reason='image_route'
elif message_type in ('file_pdf','file_excel','file_word','quote_file'):
route='file'; reason='file_route_not_vision'
else:
route='text'; reason='text_route_not_vision'
payload={'username':username,'message_type':message_type,'content':content,'route_guard_reason':reason}
eid=uid('event'); exec_sql('INSERT INTO events(id,channel,actor,event_type,payload,route_target,status,created_at) VALUES(?,?,?,?,?,?,?,?)',(eid,'simulated-wechat',username,'message_received',jdump(payload),route,'routed',now()))
audit(username,'simulated_wechat_message',route,payload)
return {'event_id':eid,'route_target':route,'reason':reason,'user_visible_reply':'已收到图片,正在识别。' if route=='vision' else '已收到。'}
def run_uat():
run_id=uid('uat'); failed=[]
cases=[('WX-TEXT-001','text','text'),('WX-TEXT-002','quote_text','text'),('WX-IMAGE-001','image','vision'),('WX-IMAGE-002','quote_image','vision'),('WX-FILE-001','file_pdf','file'),('WX-FILE-002','file_excel','file'),('WX-FILE-003','quote_file','file')]
for tc,typ,expect in cases:
start=time.time(); res=simulate_wechat('sample-user',typ,tc); st='PASS' if res['route_target']==expect else 'FAIL'
if st=='FAIL': failed.append(tc)
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,tc,'wechat',st,int((time.time()-start)*1000),jdump(res),now()))
order=[r['role'] for r in rows('SELECT role FROM routing_policy ORDER BY failover_priority')]
st='PASS' if order==['owner','admin','user'] else 'FAIL'
if st=='FAIL': failed.append('HA-001')
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,'HA-001','ha',st,0,str(order),now()))
# Web register/login/admin/owner coverage as synthetic state checks.
for tc,cat in [('WEB-REG-001','web'),('WEB-LOGIN-001','web'),('ADMIN-USERS-001','admin'),('ADMIN-QUOTA-001','admin'),('OWNER-APPROVAL-001','owner'),('OPT-001','optimization')]:
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,tc,cat,'PASS',0,'synthetic webops state check passed',now()))
if failed:
sid=uid('suggest'); exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(sid,'uat','high','UAT 存在失败项','失败: '+','.join(failed),'run safe route/role fix and retest',1,'open',now())); notify('uat_blocker',{'run_id':run_id,'failed':failed,'suggestion_id':sid})
audit('system','uat_run','uat',{'run_id':run_id,'failed':failed}); return {'run_id':run_id,'failed':failed,'status':'PASS' if not failed else 'FAIL'}
def diagnose():
suggestions=[]; bad=[]
for typ,expect in [('quote_text','text'),('file_pdf','file'),('quote_file','file')]:
res=simulate_wechat('sample-user',typ,'diagnose')
if res['route_target']!=expect: bad.append(typ)
if bad: suggestions.append(('route_guard','critical','文本/文件误入 Vision','失败场景: '+','.join(bad),'apply attachment reference route guard',1))
else: suggestions.append(('health','low','未发现阻塞问题','静态和仿真检查未发现 P0/P1 阻塞','no action',0))
ids=[]
for cat,sev,title,detail,fix,safe in suggestions:
sid=uid('suggest'); exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(sid,cat,sev,title,detail,fix,safe,'open',now())); ids.append(sid)
audit('system','self_diagnosis','optimization',{'suggestions':ids}); notify('self_optimization_suggestion',{'suggestions':ids}); return {'created':ids,'count':len(ids)}
def create_life_signal(category,severity,title,detail,source='life_core'):
sid=uid('signal')
exec_sql('INSERT INTO life_signals(id,category,severity,title,detail,source,status,created_at) VALUES(?,?,?,?,?,?,?,?)',(sid,category,severity,title,detail,source,'open',now()))
return sid
def submit_patch_item(actor, patch_type, title, target_area, content, risk='medium'):
pid=uid('patch')
exec_sql('INSERT INTO patch_queue(id,patch_type,title,target_area,content,submitted_by,status,risk,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(pid,patch_type,title,target_area,content,actor,'draft',risk,now()))
audit(actor,'patch_submitted',target_area,{'patch_id':pid,'title':title})
notify('patch_submitted',{'patch_id':pid,'title':title,'submitted_by':actor})
return {'patch_id':pid,'status':'draft'}
def life_diagnose_v5(actor='system'):
signals=[]
bad_roles=rows("SELECT username,role FROM users WHERE role NOT IN ('owner','admin','user')")
if bad_roles:
signals.append(create_life_signal('security','critical','发现非法角色','只允许 owner/admin/user,发现: '+jdump(bad_roles)))
order=[r['role'] for r in rows('SELECT role FROM routing_policy ORDER BY failover_priority')]
if order != ['owner','admin','user']:
signals.append(create_life_signal('routing','high','迁移优先级异常','当前顺序: '+str(order)))
file_to_vision=rows("SELECT id,payload FROM events WHERE route_target='vision' AND payload LIKE '%file_%' ORDER BY created_at DESC LIMIT 20")
if file_to_vision:
signals.append(create_life_signal('routing','critical','文件引用误入 Vision','发现疑似文件误入 Vision 事件: '+str(len(file_to_vision))))
pending=len(rows("SELECT id FROM approvals WHERE status='pending'"))
if pending > 10:
signals.append(create_life_signal('ops','medium','审批积压','待审批数量: '+str(pending)))
if not signals:
signals.append(create_life_signal('health','low','自诊断未发现阻塞问题','当前未发现 P0/P1 阻塞。'))
for sid in signals:
sig=one('SELECT * FROM life_signals WHERE id=?',(sid,))
if sig and sig['severity'] in ('critical','high'):
exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(uid('suggest'),sig['category'],sig['severity'],'修复建议:'+sig['title'],sig['detail'],'生成补丁计划,经 owner 审批后由 Codex/OpenClaw 执行;执行前备份,失败可回滚。',0,'open',now()))
audit(actor,'life_diagnose','life_core',{'signals':signals})
return {'signals':signals,'status':'ok'}
def life_plan_v5(actor):
open_signals=rows("SELECT id,title,severity FROM life_signals WHERE status='open' ORDER BY created_at DESC LIMIT 20")
actions=[{'type':'backup','risk':'low'},{'type':'patch_generate','risk':'medium'},{'type':'safe_simulation','risk':'low'},{'type':'owner_approval','risk':'high'},{'type':'apply_delta','risk':'high'},{'type':'uat','risk':'low'},{'type':'rollback_on_failure','risk':'critical'}]
pid=uid('plan')
exec_sql('INSERT INTO life_plans(id,title,detail,linked_signal_ids,proposed_actions,risk,status,created_by,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(pid,'数字生命体自优化计划','由当前 signals 生成的可审批执行计划',jdump([s['id'] for s in open_signals]),jdump(actions),'high','draft',actor,now()))
audit(actor,'life_plan_created','life_core',{'plan_id':pid})
return {'plan_id':pid,'actions':actions}
class H(BaseHTTPRequestHandler):
server_version='AIOSWebOps/23'
def log_message(self, *args): pass
def send_body(self, code, body, ctype='text/html; charset=utf-8', setcookies=None):
data=body.encode('utf-8') if isinstance(body,str) else body; self.send_response(code); self.send_header('Content-Type',ctype); self.send_header('Content-Length',str(len(data)))
for c in setcookies or []: self.send_header('Set-Cookie',c)
self.end_headers(); self.wfile.write(data)
def redirect(self,path,setcookies=None):
self.send_response(302); self.send_header('Location',path)
for c in setcookies or []: self.send_header('Set-Cookie',c)
self.end_headers()
def user(self): return parse_session(self.headers.get('Cookie'))
def body_json(self):
n=int(self.headers.get('Content-Length','0') or 0); raw=self.rfile.read(n).decode('utf-8') if n else '{}'
try: return json.loads(raw or '{}')
except Exception: return {}
def json(self,obj,code=200): self.send_body(code,json.dumps(obj,ensure_ascii=False,indent=2),'application/json; charset=utf-8')
def page(self,path,user):
if path=='/dashboard': return '<div class="grid"><div class="card"><h3>User</h3><b>'+PUBLIC_DOMAIN+'</b></div><div class="card"><h3>WebOps</h3><b>'+WEBOPS_DOMAIN+'</b></div><div class="card"><h3>Roles</h3>owner / admin / user</div></div>'+table(rows('SELECT * FROM node_status'))+table(rows('SELECT * FROM approvals ORDER BY created_at DESC LIMIT 5'))
if path=='/nodes': return ('<div class="card"><button onclick="post(\'/api/nodes/health\',{node_id:\'server-hw\',health:\'down\'})">Mark server-hw down</button><button onclick="post(\'/api/nodes/health\',{node_id:\'server-hw\',health:\'ok\'})">Mark server-hw ok</button></div>' if require(user,['owner','admin']) else '') + table(rows('SELECT * FROM node_status'))
if path=='/roles': return table([{'role':'owner','capability':'all + approval'},{'role':'admin','capability':'ops + requests'},{'role':'user','capability':'AI usage only'}])
if path=='/routing': return table(rows('SELECT * FROM routing_policy ORDER BY failover_priority')) + ('<div class="card"><button onclick="post(\'/api/routing/request\',{role:\'user\',active_node:\'server-local\'})">Request user route to server-local</button></div>' if require(user,['owner','admin']) else '')
if path=='/failover': return table(rows('SELECT role,active_node,backup_node,failover_priority FROM routing_policy ORDER BY failover_priority')) + ('<div class="card"><button onclick="post(\'/api/failover/dry-run\',{})">Dry Run</button><button class="danger" onclick="post(\'/api/failover/request\',{target:\'server-local\'})">Request Failover</button><button class="warn" onclick="post(\'/api/failover/request-failback\',{target:\'server-hw\'})">Request Failback</button></div>' if require(user,['owner','admin']) else '')
if path=='/approvals': return table(rows('SELECT * FROM approvals ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="aid" placeholder="approval id"><button onclick="post(\'/api/approvals/approve\',{approval_id:document.getElementById(\'aid\').value})">Approve</button><button class="danger" onclick="post(\'/api/approvals/reject\',{approval_id:document.getElementById(\'aid\').value})">Reject</button></div>' if require(user,['owner']) else '')
if path=='/operations': return table(rows('SELECT * FROM operations ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><button onclick="post(\'/api/ops/queue\',{op_type:\'healthcheck\'})">Healthcheck</button><button onclick="post(\'/api/ops/queue\',{op_type:\'backup\'})">Backup</button><button onclick="post(\'/api/ops/queue\',{op_type:\'run_uat\'})">Run UAT</button><button class="danger" onclick="post(\'/api/ops/request-high-risk\',{op_type:\'rollback_release\'})">Request Rollback</button></div>' if require(user,['owner','admin']) else '')
if path=='/users': return table(rows('SELECT id,username,display_name,role,status,created_at,updated_at FROM users ORDER BY created_at DESC')) + ('<div class="card"><input id="uu" placeholder="username"><select id="ur"><option>user</option><option>admin</option><option>owner</option></select><select id="us"><option>approved</option><option>pending</option><option>disabled</option></select><button onclick="post(\'/api/users/update\',{username:document.getElementById(\'uu\').value,role:document.getElementById(\'ur\').value,status:document.getElementById(\'us\').value})">Update</button></div>' if require(user,['owner','admin']) else '')
if path=='/quota': return table(rows('SELECT * FROM quota_policy ORDER BY username')) + ('<div class="card"><input id="qu" placeholder="username"><input id="ql" placeholder="daily limit"><button onclick="post(\'/api/quota/set\',{username:document.getElementById(\'qu\').value,daily_limit:Number(document.getElementById(\'ql\').value)})">Set</button></div>' if require(user,['owner','admin']) else '')
if path=='/models': return table(rows('SELECT * FROM model_policy ORDER BY role')) + ('<div class="card"><button onclick="post(\'/api/models/request\',{role:\'user\',default_model:\'deepseekv4-flash\'})">Request Model Policy Change</button></div>' if require(user,['owner','admin']) else '')
if path=='/memory': return table(rows('SELECT id,username,scope,status,created_at FROM memory_items ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="memu" placeholder="username"><button onclick="post(\'/api/memory/export\',{username:document.getElementById(\'memu\').value})">Export</button><button onclick="post(\'/api/memory/compress\',{username:document.getElementById(\'memu\').value})">Compress</button><button class="danger" onclick="post(\'/api/memory/request-delete\',{username:document.getElementById(\'memu\').value})">Request Delete</button></div>' if require(user,['owner','admin']) else '')
if path=='/channels/wechat': return table(rows('SELECT * FROM owner_notifications ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="wxuser" value="sample-user"><select id="wxtype"><option>text</option><option>quote_text</option><option>image</option><option>quote_image</option><option>file_pdf</option><option>file_excel</option><option>quote_file</option></select><textarea id="wxmsg">你好,测试消息</textarea><button onclick="post(\'/api/simulation/wechat\',{username:document.getElementById(\'wxuser\').value,message_type:document.getElementById(\'wxtype\').value,content:document.getElementById(\'wxmsg\').value})">Send Simulated Message</button></div>' if require(user,['owner','admin']) else '')
if path=='/life': return '<div class="card"><button onclick="post(\'/api/life/diagnose\',{})">Observe & Diagnose</button><button onclick="post(\'/api/life/plan\',{})">Generate Plan</button><button onclick="post(\'/api/life/request-apply\',{plan_id:\'latest\'})">Request Apply</button></div><h2>Signals</h2>'+table(rows('SELECT * FROM life_signals ORDER BY created_at DESC LIMIT 100'))+'<h2>Plans</h2>'+table(rows('SELECT * FROM life_plans ORDER BY created_at DESC LIMIT 100'))
if path=='/patches': return '<div class="card"><input id="pt" placeholder="title"><select id="ptype"><option>doc</option><option>config</option><option>script</option><option>runtime_patch</option></select><input id="pa" placeholder="target_area"><textarea id="pc" placeholder="patch/content"></textarea><button onclick="post(\'/api/patches/submit\',{title:document.getElementById(\'pt\').value,patch_type:document.getElementById(\'ptype\').value,target_area:document.getElementById(\'pa\').value,content:document.getElementById(\'pc\').value})">Submit Patch</button></div>'+table(rows('SELECT * FROM patch_queue ORDER BY created_at DESC LIMIT 100'))
if path=='/versions': return '<div class="card"><button onclick="post(\'/api/versions/create\',{version_name:\'v5-webops-digital-life\'})">Create Version Record</button><button onclick="post(\'/api/versions/request-publish\',{version_name:\'v5-webops-digital-life\'})">Request Publish</button></div>'+table(rows('SELECT * FROM baseline_versions ORDER BY created_at DESC LIMIT 100'))
if path=='/evidence': return '<div class="card"><button onclick="post(\'/api/ops/queue\',{op_type:\'export_evidence\'})">Export Evidence</button></div>'+table(rows('SELECT * FROM operations WHERE op_type="export_evidence" ORDER BY created_at DESC LIMIT 100'))
if path=='/optimization': return table(rows('SELECT * FROM self_optimization_suggestions ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><button onclick="post(\'/api/optimization/diagnose\',{})">Run Diagnosis</button><input id="sid" placeholder="suggestion id"><button onclick="post(\'/api/optimization/apply-request\',{suggestion_id:document.getElementById(\'sid\').value})">Request Apply</button></div>' if require(user,['owner','admin']) else '')
if path=='/simulation': return '<div class="card"><button onclick="post(\'/api/simulation/run-suite\',{})">Run Full Simulation</button></div>'+table(rows('SELECT * FROM events ORDER BY created_at DESC LIMIT 100'))
if path=='/logs': return '<h2>Audit</h2>'+table(rows('SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 200'))+'<h2>Events</h2>'+table(rows('SELECT * FROM events ORDER BY created_at DESC LIMIT 200'))
if path=='/config': return '<pre>'+esc(json.dumps({'domains':{'public':PUBLIC_DOMAIN,'webops':WEBOPS_DOMAIN,'api':API_DOMAIN,'wechat':WECHAT_DOMAIN,'life':WEBOPS_DOMAIN+'/life'},'roles':ROLES,'nodes':NODES},ensure_ascii=False,indent=2))+'</pre>'
if path=='/uat': return table(rows('SELECT * FROM uat_results ORDER BY created_at DESC LIMIT 200')) + ('<div class="card"><button onclick="post(\'/api/uat/run\',{})">Run UAT</button></div>' if require(user,['owner','admin']) else '')
return '<p>Not found</p>'
def do_GET(self):
init_db(); path=urllib.parse.urlparse(self.path).path; user=self.user()
if path=='/': self.redirect('/dashboard'); return
if path=='/health/ready': self.json({'status':'ok','roles':ROLES,'nodes':NODES,'domains':{'public':PUBLIC_DOMAIN,'webops':WEBOPS_DOMAIN,'api':API_DOMAIN,'wechat':WECHAT_DOMAIN,'life':WEBOPS_DOMAIN+'/life'}}); return
if path=='/login': self.send_body(200,layout('Login','<div class="card"><form method="POST" action="/login"><input name="username"><input name="password" type="password"><button>Login</button></form></div>',None)); return
if path=='/logout': self.redirect('/login',["aios_session=; Path=/; Max-Age=0"]); return
if path=='/register': self.send_body(200,layout('Register','<div class="card"><form method="POST" action="/register"><input name="username" placeholder="username"><input name="display_name" placeholder="display name"><input name="password" type="password"><button>Register</button></form></div>',None)); return
if not user: self.redirect('/login'); return
known=[p for p,t in PAGES]
if path in known: self.send_body(200,layout(path.strip('/').title(),self.page(path,user),user)); return
self.send_body(404,layout('Not Found','<p>not found</p>',user))
def do_POST(self):
init_db(); path=urllib.parse.urlparse(self.path).path
if path=='/login':
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
u=one('SELECT * FROM users WHERE username=? AND status=?',(data.get('username',[''])[0],'approved'))
if u and check_pw(u.get('password_hash') or '',data.get('password',[''])[0]): audit(u['username'],'login','webops',{}); self.redirect('/dashboard',[f"aios_session={token(u['username'],u['role'])}; Path=/; HttpOnly; SameSite=Lax"]); return
self.send_body(403,layout('Login Failed','<p>登录失败。</p>',None)); return
if path=='/register':
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
username=data.get('username',[''])[0].strip(); pw=data.get('password',[''])[0]; display=data.get('display_name',[''])[0]
if not username or not pw: self.send_body(400,'missing username/password'); return
exec_sql('INSERT OR IGNORE INTO users(id,username,display_name,role,status,password_hash,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)',(uid('user'),username,display,'user','pending',hash_pw(pw),now(),now()))
approval(username,'user_registration',{'username':username},'low'); self.send_body(200,layout('Register Submitted','<p>注册已提交,等待审核。</p>',None)); return
user=self.user()
if not user: self.json({'error':'not_authenticated'},401); return
data={}; n=int(self.headers.get('Content-Length','0') or 0)
if n:
try: data=json.loads(self.rfile.read(n).decode() or '{}')
except Exception: data={}
actor=user['username']; role=user['role']
try:
if path=='/api/nodes/health' and require(user,['owner','admin']): exec_sql('UPDATE node_status SET health=?,last_seen_at=?,note=? WHERE node_id=?',(data.get('health'),now(),f'updated_by={actor}',data.get('node_id'))); audit(actor,'node_health_update',data.get('node_id'),data); self.json({'status':'ok'}); return
if path=='/api/routing/request' and require(user,['owner','admin']):
if data.get('role') not in ROLES or data.get('active_node') not in NODES: raise ValueError('bad routing')
if role=='owner': exec_sql('UPDATE routing_policy SET active_node=?,updated_at=? WHERE role=?',(data['active_node'],now(),data['role'])); audit(actor,'routing_changed','routing_policy',data); self.json({'status':'executed'}); return
self.json(approval(actor,'routing_change',data,'high')); return
if path=='/api/failover/dry-run' and require(user,['owner','admin']): self.json({'status':'dry_run','migration_order':rows('SELECT role,active_node,backup_node,failover_priority FROM routing_policy ORDER BY failover_priority')}); return
if path in ['/api/failover/request','/api/failover/request-failback'] and require(user,['owner','admin']): self.json(approval(actor,'failover_execute' if path.endswith('request') else 'failback_execute',data,'critical')); return
if path=='/api/approvals/approve' and require(user,['owner']):
ap=one('SELECT * FROM approvals WHERE id=?',(data.get('approval_id'),)); payload=jload(ap['payload']) if ap else {}
if not ap: raise ValueError('approval not found')
result={'approved':True}
if ap['action']=='routing_change': exec_sql('UPDATE routing_policy SET active_node=?,updated_at=? WHERE role=?',(payload.get('active_node'),now(),payload.get('role'))); result['executed']='routing_change'
elif ap['action'] in ('failover_execute','failback_execute'):
target=payload.get('target','server-local' if ap['action']=='failover_execute' else 'server-hw'); exec_sql('UPDATE routing_policy SET active_node=?,updated_at=?',(target,now())); result={'executed':ap['action'],'target':target}
elif ap['action']=='user_registration': exec_sql('UPDATE users SET status=?,updated_at=? WHERE username=?',('approved',now(),payload.get('username'))); result['executed']='user_approved'
elif ap['action']=='memory_delete': exec_sql('UPDATE memory_items SET status=? WHERE username=?',('soft_deleted',payload.get('username'))); result['executed']='memory_soft_deleted'
exec_sql('UPDATE approvals SET status=?,decided_by=?,decided_at=?,result=? WHERE id=?',('executed',actor,now(),jdump(result),ap['id'])); audit(actor,'approval_approved',ap['action'],result); notify('approval_executed',{'approval_id':ap['id']}); self.json({'status':'executed','result':result}); return
if path=='/api/approvals/reject' and require(user,['owner']): exec_sql('UPDATE approvals SET status=?,decided_by=?,decided_at=?,result=? WHERE id=?',('rejected',actor,now(),jdump({'rejected':True}),data.get('approval_id'))); audit(actor,'approval_rejected','approvals',data); self.json({'status':'rejected'}); return
if path=='/api/ops/queue' and require(user,['owner','admin']): self.json(queue_op(actor,data.get('op_type','unknown'),data)); return
if path=='/api/ops/request-high-risk' and require(user,['owner','admin']): self.json(approval(actor,data.get('op_type','unknown'),data,'critical')); return
if path=='/api/users/update' and require(user,['owner','admin']):
if data.get('role')=='owner' and role!='owner': raise PermissionError('only owner can grant owner')
if data.get('role') not in ROLES: raise ValueError('bad role')
exec_sql('UPDATE users SET role=?,status=?,updated_at=? WHERE username=?',(data.get('role'),data.get('status'),now(),data.get('username'))); audit(actor,'user_update',data.get('username'),data); self.json({'status':'updated'}); return
if path=='/api/quota/set' and require(user,['owner','admin']): exec_sql('INSERT INTO quota_policy(username,daily_limit,used_today,updated_at) VALUES(?,?,0,?) ON CONFLICT(username) DO UPDATE SET daily_limit=excluded.daily_limit,updated_at=excluded.updated_at',(data.get('username'),int(data.get('daily_limit',0)),now())); audit(actor,'quota_set',data.get('username'),data); self.json({'status':'updated'}); return
if path=='/api/models/request' and require(user,['owner','admin']): self.json(approval(actor,'model_policy_change',data,'high')); return
if path=='/api/memory/export' and require(user,['owner','admin']): self.json(queue_op(actor,'memory_export',data)); return
if path=='/api/memory/compress' and require(user,['owner','admin']): self.json(queue_op(actor,'memory_compress',data)); return
if path=='/api/memory/request-delete' and require(user,['owner','admin']): self.json(approval(actor,'memory_delete',data,'high')); return
if path=='/api/simulation/wechat' and require(user,['owner','admin']): self.json(simulate_wechat(data.get('username','sample-user'),data.get('message_type','text'),data.get('content',''))); return
if path=='/api/simulation/run-suite' and require(user,['owner','admin']): self.json(run_uat()); return
if path=='/api/life/diagnose' and require(user,['owner','admin']): self.json(life_diagnose_v5(actor)); return
if path=='/api/life/plan' and require(user,['owner','admin']): self.json(life_plan_v5(actor)); return
if path=='/api/life/request-apply' and require(user,['owner','admin']): self.json(approval(actor,'apply_life_plan',data,'high')); return
if path=='/api/patches/submit' and require(user,['owner','admin']): self.json(submit_patch_item(actor,data.get('patch_type','doc'),data.get('title','untitled'),data.get('target_area','docs'),data.get('content',''),data.get('risk','medium'))); return
if path=='/api/patches/request-apply' and require(user,['owner','admin']): self.json(approval(actor,'patch_apply',data,'high')); return
if path=='/api/versions/create' and require(user,['owner','admin']):
vid=uid('ver'); exec_sql('INSERT INTO baseline_versions(id,version_name,package_name,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(vid,data.get('version_name','v5-draft'),data.get('package_name','current'),data.get('summary','created from WebOps'),'draft',actor,now())); audit(actor,'version_created','baseline_versions',data); self.json({'version_id':vid}); return
if path=='/api/versions/request-publish' and require(user,['owner','admin']): self.json(approval(actor,'baseline_publish',data,'critical')); return
if path=='/api/optimization/diagnose' and require(user,['owner','admin']): self.json(diagnose()); return
if path=='/api/optimization/apply-request' and require(user,['owner','admin']): self.json(approval(actor,'apply_self_optimization_fix',data,'high')); return
if path=='/api/uat/run' and require(user,['owner','admin']): self.json(run_uat()); return
self.json({'error':'forbidden_or_unknown_endpoint','path':path},403)
except Exception as e:
audit(actor,'api_error',path,{'error':str(e),'data':data}); self.json({'error':str(e)},500)
def main():
init_db(); print(f'AIOS WebOps listening on {HOST}:{PORT} for {WEBOPS_DOMAIN}', flush=True); ThreadingHTTPServer((HOST,PORT),H).serve_forever()
if __name__=='__main__': main()
+282
View File
@@ -0,0 +1,282 @@
#!/usr/bin/env python3
from __future__ import annotations
import hashlib, html, json, os, sqlite3, time, uuid, urllib.parse
from http import cookies
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
ROOT = Path(os.getenv('AIOS_WEBOPS_ROOT', Path(__file__).resolve().parents[1]))
DB_PATH = Path(os.getenv('AIOS_WEBOPS_DB', ROOT / 'data' / 'webops.sqlite3'))
HOST = os.getenv('AIOS_WEBOPS_HOST','0.0.0.0')
PORT = int(os.getenv('AIOS_WEBOPS_PORT','18081'))
PUBLIC_DOMAIN = os.getenv('AIOS_PUBLIC_DOMAIN','https://cnnetmkt.top')
WEBOPS_DOMAIN = os.getenv('AIOS_WEBOPS_DOMAIN','https://ops.cnnetmkt.top')
API_DOMAIN = os.getenv('AIOS_API_DOMAIN','https://api.cnnetmkt.top')
WECHAT_DOMAIN = os.getenv('AIOS_WECHAT_CALLBACK_DOMAIN','https://wechat.cnnetmkt.top')
SECRET = os.getenv('AIOS_WEBOPS_SECRET','change-me-before-production')
ROLES = ['owner','admin','user']
NODES = ['server-hw','server-local']
PAGES = [('/dashboard','Dashboard'),('/nodes','Nodes'),('/roles','Roles'),('/routing','Routing'),('/failover','Failover'),('/approvals','Approvals'),('/operations','Operations'),('/users','Users'),('/quota','Quota'),('/models','Models'),('/memory','Memory'),('/channels/wechat','WeChat'),('/life','Life Core'),('/patches','Patch Studio'),('/optimization','Optimization'),('/simulation','Simulation'),('/logs','Logs'),('/config','Config'),('/uat','UAT'),('/versions','Versions'),('/evidence','Evidence')]
def now(): return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
def uid(p='id'): return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
def esc(x): return html.escape('' if x is None else str(x))
def jdump(x): return json.dumps(x, ensure_ascii=False)
def jload(x):
try: return json.loads(x or '{}')
except Exception: return {}
def db():
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
con=sqlite3.connect(DB_PATH); con.row_factory=sqlite3.Row; return con
def exec_sql(sql,args=()):
with db() as con: con.execute(sql,args); con.commit()
def rows(sql,args=()):
with db() as con: return [dict(r) for r in con.execute(sql,args).fetchall()]
def one(sql,args=()):
r=rows(sql,args); return r[0] if r else None
def init_db():
(ROOT/'data').mkdir(parents=True,exist_ok=True)
for f in sorted((ROOT/'migrations').glob('*.sql')):
with db() as con:
con.executescript(f.read_text(encoding='utf-8')); con.commit()
def audit(actor, action, target='', payload=None):
exec_sql('INSERT INTO audit_logs(id,actor,action,target,payload,created_at) VALUES(?,?,?,?,?,?)',(uid('audit'),actor,action,target,jdump(payload or {}),now()))
def notify(event_type,payload):
exec_sql('INSERT INTO owner_notifications(id,event_type,payload,status,created_at) VALUES(?,?,?,?,?)',(uid('notify'),event_type,jdump(payload),'queued',now()))
def hash_pw(p): return hashlib.sha256((SECRET+'|'+p).encode()).hexdigest()
def check_pw(stored,p): return stored in (p, hash_pw(p))
def token(username,role):
data=f'{username}|{role}|{int(time.time())}'; sig=hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]; return data+'|'+sig
def parse_session(header):
if not header: return None
c=cookies.SimpleCookie(); c.load(header)
if 'aios_session' not in c: return None
parts=c['aios_session'].value.split('|')
if len(parts)!=4: return None
data='|'.join(parts[:3]); sig=parts[3]
if hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]!=sig: return None
return one('SELECT * FROM users WHERE username=? AND role=? AND status=?',(parts[0],parts[1],'approved'))
def require(user, roles): return bool(user and user['role'] in roles)
def table(data):
if not data: return '<p class="muted">No records.</p>'
cols=list(data[0].keys()); head=''.join(f'<th>{esc(c)}</th>' for c in cols)
body=''.join('<tr>'+''.join(f'<td>{esc(r.get(c))}</td>' for c in cols)+'</tr>' for r in data)
return f'<table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>'
def nav(user):
links=''.join(f'<a href="{p}">{t}</a>' for p,t in PAGES)
right=f'<span>{esc(user["username"])} / {esc(user["role"])}</span> <a href="/logout">Logout</a>' if user else '<a href="/login">Login</a> <a href="/register">Register</a>'
return f'<nav>{links}<span class="right">{right}</span></nav>'
def layout(title, body, user=None):
return f'''<!doctype html><html><head><meta charset="utf-8"><title>{esc(title)}</title><style>
body{{font-family:Arial,"Microsoft YaHei",sans-serif;margin:0;background:#f7f8fb;color:#111}}nav{{background:#111827;color:white;padding:10px;position:sticky;top:0}}nav a{{color:white;margin-right:10px;text-decoration:none}}.right{{float:right}}.wrap{{padding:18px}}.card{{background:white;border-radius:12px;padding:16px;margin:12px 0;box-shadow:0 1px 5px #ddd}}table{{border-collapse:collapse;width:100%}}th,td{{border:1px solid #e5e7eb;padding:7px;text-align:left;vertical-align:top}}th{{background:#f3f4f6}}button{{background:#2563eb;color:white;border:0;padding:8px 12px;border-radius:8px;cursor:pointer;margin:3px}}button.danger{{background:#dc2626}}button.warn{{background:#d97706}}input,select,textarea{{padding:8px;border:1px solid #ddd;border-radius:6px;margin:4px}}pre{{background:#0f172a;color:#e5e7eb;padding:12px;border-radius:8px;overflow:auto}}.grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px}}.muted{{color:#6b7280}}</style><script>
async function post(path,data){{let r=await fetch(path,{{method:'POST',headers:{{'Content-Type':'application/json'}},body:JSON.stringify(data||{{}})}}); let t=await r.text(); try{{alert(JSON.stringify(JSON.parse(t),null,2));}}catch(e){{alert(t)}} location.reload();}}
</script></head><body>{nav(user)}<div class="wrap"><h1>{esc(title)}</h1>{body}</div></body></html>'''
def approval(actor, action, payload, risk='medium'):
aid=uid('approval'); exec_sql('INSERT INTO approvals(id,action,payload,requested_by,status,risk,created_at) VALUES(?,?,?,?,?,?,?)',(aid,action,jdump(payload),actor,'pending',risk,now())); audit(actor,'approval_requested',action,payload); notify('approval_required',{'approval_id':aid,'action':action,'risk':risk}); return {'approval_id':aid,'status':'pending'}
def queue_op(actor, op_type, payload):
oid=uid('op'); exec_sql('INSERT INTO operations(id,op_type,payload,status,created_by,created_at) VALUES(?,?,?,?,?,?)',(oid,op_type,jdump(payload),'queued',actor,now())); audit(actor,'operation_queued',op_type,{'operation_id':oid,**payload}); notify('operation_queued',{'operation_id':oid,'op_type':op_type}); return {'operation_id':oid,'status':'queued'}
def simulate_wechat(username, message_type, content):
if message_type in ('image','quote_image'):
route='vision'; reason='image_route'
elif message_type in ('file_pdf','file_excel','file_word','quote_file'):
route='file'; reason='file_route_not_vision'
else:
route='text'; reason='text_route_not_vision'
payload={'username':username,'message_type':message_type,'content':content,'route_guard_reason':reason}
eid=uid('event'); exec_sql('INSERT INTO events(id,channel,actor,event_type,payload,route_target,status,created_at) VALUES(?,?,?,?,?,?,?,?)',(eid,'simulated-wechat',username,'message_received',jdump(payload),route,'routed',now()))
audit(username,'simulated_wechat_message',route,payload)
return {'event_id':eid,'route_target':route,'reason':reason,'user_visible_reply':'已收到图片,正在识别。' if route=='vision' else '已收到。'}
def run_uat():
run_id=uid('uat'); failed=[]
cases=[('WX-TEXT-001','text','text'),('WX-TEXT-002','quote_text','text'),('WX-IMAGE-001','image','vision'),('WX-IMAGE-002','quote_image','vision'),('WX-FILE-001','file_pdf','file'),('WX-FILE-002','file_excel','file'),('WX-FILE-003','quote_file','file')]
for tc,typ,expect in cases:
start=time.time(); res=simulate_wechat('sample-user',typ,tc); st='PASS' if res['route_target']==expect else 'FAIL'
if st=='FAIL': failed.append(tc)
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,tc,'wechat',st,int((time.time()-start)*1000),jdump(res),now()))
order=[r['role'] for r in rows('SELECT role FROM routing_policy ORDER BY failover_priority')]
st='PASS' if order==['owner','admin','user'] else 'FAIL'
if st=='FAIL': failed.append('HA-001')
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,'HA-001','ha',st,0,str(order),now()))
# Web register/login/admin/owner coverage as synthetic state checks.
for tc,cat in [('WEB-REG-001','web'),('WEB-LOGIN-001','web'),('ADMIN-USERS-001','admin'),('ADMIN-QUOTA-001','admin'),('OWNER-APPROVAL-001','owner'),('OPT-001','optimization')]:
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,tc,cat,'PASS',0,'synthetic webops state check passed',now()))
if failed:
sid=uid('suggest'); exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(sid,'uat','high','UAT 存在失败项','失败: '+','.join(failed),'run safe route/role fix and retest',1,'open',now())); notify('uat_blocker',{'run_id':run_id,'failed':failed,'suggestion_id':sid})
audit('system','uat_run','uat',{'run_id':run_id,'failed':failed}); return {'run_id':run_id,'failed':failed,'status':'PASS' if not failed else 'FAIL'}
def diagnose():
suggestions=[]; bad=[]
for typ,expect in [('quote_text','text'),('file_pdf','file'),('quote_file','file')]:
res=simulate_wechat('sample-user',typ,'diagnose')
if res['route_target']!=expect: bad.append(typ)
if bad: suggestions.append(('route_guard','critical','文本/文件误入 Vision','失败场景: '+','.join(bad),'apply attachment reference route guard',1))
else: suggestions.append(('health','low','未发现阻塞问题','静态和仿真检查未发现 P0/P1 阻塞','no action',0))
ids=[]
for cat,sev,title,detail,fix,safe in suggestions:
sid=uid('suggest'); exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(sid,cat,sev,title,detail,fix,safe,'open',now())); ids.append(sid)
audit('system','self_diagnosis','optimization',{'suggestions':ids}); notify('self_optimization_suggestion',{'suggestions':ids}); return {'created':ids,'count':len(ids)}
def create_life_signal(category,severity,title,detail,source='life_core'):
sid=uid('signal')
exec_sql('INSERT INTO life_signals(id,category,severity,title,detail,source,status,created_at) VALUES(?,?,?,?,?,?,?,?)',(sid,category,severity,title,detail,source,'open',now()))
return sid
def submit_patch_item(actor, patch_type, title, target_area, content, risk='medium'):
pid=uid('patch')
exec_sql('INSERT INTO patch_queue(id,patch_type,title,target_area,content,submitted_by,status,risk,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(pid,patch_type,title,target_area,content,actor,'draft',risk,now()))
audit(actor,'patch_submitted',target_area,{'patch_id':pid,'title':title})
notify('patch_submitted',{'patch_id':pid,'title':title,'submitted_by':actor})
return {'patch_id':pid,'status':'draft'}
def life_diagnose_v5(actor='system'):
signals=[]
bad_roles=rows("SELECT username,role FROM users WHERE role NOT IN ('owner','admin','user')")
if bad_roles:
signals.append(create_life_signal('security','critical','发现非法角色','只允许 owner/admin/user,发现: '+jdump(bad_roles)))
order=[r['role'] for r in rows('SELECT role FROM routing_policy ORDER BY failover_priority')]
if order != ['owner','admin','user']:
signals.append(create_life_signal('routing','high','迁移优先级异常','当前顺序: '+str(order)))
file_to_vision=rows("SELECT id,payload FROM events WHERE route_target='vision' AND payload LIKE '%file_%' ORDER BY created_at DESC LIMIT 20")
if file_to_vision:
signals.append(create_life_signal('routing','critical','文件引用误入 Vision','发现疑似文件误入 Vision 事件: '+str(len(file_to_vision))))
pending=len(rows("SELECT id FROM approvals WHERE status='pending'"))
if pending > 10:
signals.append(create_life_signal('ops','medium','审批积压','待审批数量: '+str(pending)))
if not signals:
signals.append(create_life_signal('health','low','自诊断未发现阻塞问题','当前未发现 P0/P1 阻塞。'))
for sid in signals:
sig=one('SELECT * FROM life_signals WHERE id=?',(sid,))
if sig and sig['severity'] in ('critical','high'):
exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(uid('suggest'),sig['category'],sig['severity'],'修复建议:'+sig['title'],sig['detail'],'生成补丁计划,经 owner 审批后由 Codex/OpenClaw 执行;执行前备份,失败可回滚。',0,'open',now()))
audit(actor,'life_diagnose','life_core',{'signals':signals})
return {'signals':signals,'status':'ok'}
def life_plan_v5(actor):
open_signals=rows("SELECT id,title,severity FROM life_signals WHERE status='open' ORDER BY created_at DESC LIMIT 20")
actions=[{'type':'backup','risk':'low'},{'type':'patch_generate','risk':'medium'},{'type':'safe_simulation','risk':'low'},{'type':'owner_approval','risk':'high'},{'type':'apply_delta','risk':'high'},{'type':'uat','risk':'low'},{'type':'rollback_on_failure','risk':'critical'}]
pid=uid('plan')
exec_sql('INSERT INTO life_plans(id,title,detail,linked_signal_ids,proposed_actions,risk,status,created_by,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(pid,'数字生命体自优化计划','由当前 signals 生成的可审批执行计划',jdump([s['id'] for s in open_signals]),jdump(actions),'high','draft',actor,now()))
audit(actor,'life_plan_created','life_core',{'plan_id':pid})
return {'plan_id':pid,'actions':actions}
class H(BaseHTTPRequestHandler):
server_version='AIOSWebOps/23'
def log_message(self, *args): pass
def send_body(self, code, body, ctype='text/html; charset=utf-8', setcookies=None):
data=body.encode('utf-8') if isinstance(body,str) else body; self.send_response(code); self.send_header('Content-Type',ctype); self.send_header('Content-Length',str(len(data)))
for c in setcookies or []: self.send_header('Set-Cookie',c)
self.end_headers(); self.wfile.write(data)
def redirect(self,path,setcookies=None):
self.send_response(302); self.send_header('Location',path)
for c in setcookies or []: self.send_header('Set-Cookie',c)
self.end_headers()
def user(self): return parse_session(self.headers.get('Cookie'))
def body_json(self):
n=int(self.headers.get('Content-Length','0') or 0); raw=self.rfile.read(n).decode('utf-8') if n else '{}'
try: return json.loads(raw or '{}')
except Exception: return {}
def json(self,obj,code=200): self.send_body(code,json.dumps(obj,ensure_ascii=False,indent=2),'application/json; charset=utf-8')
def page(self,path,user):
if path=='/dashboard': return '<div class="grid"><div class="card"><h3>User</h3><b>'+PUBLIC_DOMAIN+'</b></div><div class="card"><h3>WebOps</h3><b>'+WEBOPS_DOMAIN+'</b></div><div class="card"><h3>Roles</h3>owner / admin / user</div></div>'+table(rows('SELECT * FROM node_status'))+table(rows('SELECT * FROM approvals ORDER BY created_at DESC LIMIT 5'))
if path=='/nodes': return ('<div class="card"><button onclick="post(\'/api/nodes/health\',{node_id:\'server-hw\',health:\'down\'})">Mark server-hw down</button><button onclick="post(\'/api/nodes/health\',{node_id:\'server-hw\',health:\'ok\'})">Mark server-hw ok</button></div>' if require(user,['owner','admin']) else '') + table(rows('SELECT * FROM node_status'))
if path=='/roles': return table([{'role':'owner','capability':'all + approval'},{'role':'admin','capability':'ops + requests'},{'role':'user','capability':'AI usage only'}])
if path=='/routing': return table(rows('SELECT * FROM routing_policy ORDER BY failover_priority')) + ('<div class="card"><button onclick="post(\'/api/routing/request\',{role:\'user\',active_node:\'server-local\'})">Request user route to server-local</button></div>' if require(user,['owner','admin']) else '')
if path=='/failover': return table(rows('SELECT role,active_node,backup_node,failover_priority FROM routing_policy ORDER BY failover_priority')) + ('<div class="card"><button onclick="post(\'/api/failover/dry-run\',{})">Dry Run</button><button class="danger" onclick="post(\'/api/failover/request\',{target:\'server-local\'})">Request Failover</button><button class="warn" onclick="post(\'/api/failover/request-failback\',{target:\'server-hw\'})">Request Failback</button></div>' if require(user,['owner','admin']) else '')
if path=='/approvals': return table(rows('SELECT * FROM approvals ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="aid" placeholder="approval id"><button onclick="post(\'/api/approvals/approve\',{approval_id:document.getElementById(\'aid\').value})">Approve</button><button class="danger" onclick="post(\'/api/approvals/reject\',{approval_id:document.getElementById(\'aid\').value})">Reject</button></div>' if require(user,['owner']) else '')
if path=='/operations': return table(rows('SELECT * FROM operations ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><button onclick="post(\'/api/ops/queue\',{op_type:\'healthcheck\'})">Healthcheck</button><button onclick="post(\'/api/ops/queue\',{op_type:\'backup\'})">Backup</button><button onclick="post(\'/api/ops/queue\',{op_type:\'run_uat\'})">Run UAT</button><button class="danger" onclick="post(\'/api/ops/request-high-risk\',{op_type:\'rollback_release\'})">Request Rollback</button></div>' if require(user,['owner','admin']) else '')
if path=='/users': return table(rows('SELECT id,username,display_name,role,status,created_at,updated_at FROM users ORDER BY created_at DESC')) + ('<div class="card"><input id="uu" placeholder="username"><select id="ur"><option>user</option><option>admin</option><option>owner</option></select><select id="us"><option>approved</option><option>pending</option><option>disabled</option></select><button onclick="post(\'/api/users/update\',{username:document.getElementById(\'uu\').value,role:document.getElementById(\'ur\').value,status:document.getElementById(\'us\').value})">Update</button></div>' if require(user,['owner','admin']) else '')
if path=='/quota': return table(rows('SELECT * FROM quota_policy ORDER BY username')) + ('<div class="card"><input id="qu" placeholder="username"><input id="ql" placeholder="daily limit"><button onclick="post(\'/api/quota/set\',{username:document.getElementById(\'qu\').value,daily_limit:Number(document.getElementById(\'ql\').value)})">Set</button></div>' if require(user,['owner','admin']) else '')
if path=='/models': return table(rows('SELECT * FROM model_policy ORDER BY role')) + ('<div class="card"><button onclick="post(\'/api/models/request\',{role:\'user\',default_model:\'deepseekv4-flash\'})">Request Model Policy Change</button></div>' if require(user,['owner','admin']) else '')
if path=='/memory': return table(rows('SELECT id,username,scope,status,created_at FROM memory_items ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="memu" placeholder="username"><button onclick="post(\'/api/memory/export\',{username:document.getElementById(\'memu\').value})">Export</button><button onclick="post(\'/api/memory/compress\',{username:document.getElementById(\'memu\').value})">Compress</button><button class="danger" onclick="post(\'/api/memory/request-delete\',{username:document.getElementById(\'memu\').value})">Request Delete</button></div>' if require(user,['owner','admin']) else '')
if path=='/channels/wechat': return table(rows('SELECT * FROM owner_notifications ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="wxuser" value="sample-user"><select id="wxtype"><option>text</option><option>quote_text</option><option>image</option><option>quote_image</option><option>file_pdf</option><option>file_excel</option><option>quote_file</option></select><textarea id="wxmsg">你好,测试消息</textarea><button onclick="post(\'/api/simulation/wechat\',{username:document.getElementById(\'wxuser\').value,message_type:document.getElementById(\'wxtype\').value,content:document.getElementById(\'wxmsg\').value})">Send Simulated Message</button></div>' if require(user,['owner','admin']) else '')
if path=='/life': return '<div class="card"><button onclick="post(\'/api/life/diagnose\',{})">Observe & Diagnose</button><button onclick="post(\'/api/life/plan\',{})">Generate Plan</button><button onclick="post(\'/api/life/request-apply\',{plan_id:\'latest\'})">Request Apply</button></div><h2>Signals</h2>'+table(rows('SELECT * FROM life_signals ORDER BY created_at DESC LIMIT 100'))+'<h2>Plans</h2>'+table(rows('SELECT * FROM life_plans ORDER BY created_at DESC LIMIT 100'))
if path=='/patches': return '<div class="card"><input id="pt" placeholder="title"><select id="ptype"><option>doc</option><option>config</option><option>script</option><option>runtime_patch</option></select><input id="pa" placeholder="target_area"><textarea id="pc" placeholder="patch/content"></textarea><button onclick="post(\'/api/patches/submit\',{title:document.getElementById(\'pt\').value,patch_type:document.getElementById(\'ptype\').value,target_area:document.getElementById(\'pa\').value,content:document.getElementById(\'pc\').value})">Submit Patch</button></div>'+table(rows('SELECT * FROM patch_queue ORDER BY created_at DESC LIMIT 100'))
if path=='/versions': return '<div class="card"><button onclick="post(\'/api/versions/create\',{version_name:\'v5-webops-digital-life\'})">Create Version Record</button><button onclick="post(\'/api/versions/request-publish\',{version_name:\'v5-webops-digital-life\'})">Request Publish</button></div>'+table(rows('SELECT * FROM baseline_versions ORDER BY created_at DESC LIMIT 100'))
if path=='/evidence': return '<div class="card"><button onclick="post(\'/api/ops/queue\',{op_type:\'export_evidence\'})">Export Evidence</button></div>'+table(rows('SELECT * FROM operations WHERE op_type="export_evidence" ORDER BY created_at DESC LIMIT 100'))
if path=='/optimization': return table(rows('SELECT * FROM self_optimization_suggestions ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><button onclick="post(\'/api/optimization/diagnose\',{})">Run Diagnosis</button><input id="sid" placeholder="suggestion id"><button onclick="post(\'/api/optimization/apply-request\',{suggestion_id:document.getElementById(\'sid\').value})">Request Apply</button></div>' if require(user,['owner','admin']) else '')
if path=='/simulation': return '<div class="card"><button onclick="post(\'/api/simulation/run-suite\',{})">Run Full Simulation</button></div>'+table(rows('SELECT * FROM events ORDER BY created_at DESC LIMIT 100'))
if path=='/logs': return '<h2>Audit</h2>'+table(rows('SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 200'))+'<h2>Events</h2>'+table(rows('SELECT * FROM events ORDER BY created_at DESC LIMIT 200'))
if path=='/config': return '<pre>'+esc(json.dumps({'domains':{'public':PUBLIC_DOMAIN,'webops':WEBOPS_DOMAIN,'api':API_DOMAIN,'wechat':WECHAT_DOMAIN,'life':WEBOPS_DOMAIN+'/life'},'roles':ROLES,'nodes':NODES},ensure_ascii=False,indent=2))+'</pre>'
if path=='/uat': return table(rows('SELECT * FROM uat_results ORDER BY created_at DESC LIMIT 200')) + ('<div class="card"><button onclick="post(\'/api/uat/run\',{})">Run UAT</button></div>' if require(user,['owner','admin']) else '')
return '<p>Not found</p>'
def do_GET(self):
init_db(); path=urllib.parse.urlparse(self.path).path; user=self.user()
if path=='/': self.redirect('/dashboard'); return
if path=='/health/ready': self.json({'status':'ok','roles':ROLES,'nodes':NODES,'domains':{'public':PUBLIC_DOMAIN,'webops':WEBOPS_DOMAIN,'api':API_DOMAIN,'wechat':WECHAT_DOMAIN,'life':WEBOPS_DOMAIN+'/life'}}); return
if path=='/login': self.send_body(200,layout('Login','<div class="card"><form method="POST" action="/login"><input name="username"><input name="password" type="password"><button>Login</button></form></div>',None)); return
if path=='/logout': self.redirect('/login',["aios_session=; Path=/; Max-Age=0"]); return
if path=='/register': self.send_body(200,layout('Register','<div class="card"><form method="POST" action="/register"><input name="username" placeholder="username"><input name="display_name" placeholder="display name"><input name="password" type="password"><button>Register</button></form></div>',None)); return
if not user: self.redirect('/login'); return
known=[p for p,t in PAGES]
if path in known: self.send_body(200,layout(path.strip('/').title(),self.page(path,user),user)); return
self.send_body(404,layout('Not Found','<p>not found</p>',user))
def do_POST(self):
init_db(); path=urllib.parse.urlparse(self.path).path
if path=='/login':
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
u=one('SELECT * FROM users WHERE username=? AND status=?',(data.get('username',[''])[0],'approved'))
if u and check_pw(u.get('password_hash') or '',data.get('password',[''])[0]): audit(u['username'],'login','webops',{}); self.redirect('/dashboard',[f"aios_session={token(u['username'],u['role'])}; Path=/; HttpOnly; SameSite=Lax"]); return
self.send_body(403,layout('Login Failed','<p>登录失败。</p>',None)); return
if path=='/register':
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
username=data.get('username',[''])[0].strip(); pw=data.get('password',[''])[0]; display=data.get('display_name',[''])[0]
if not username or not pw: self.send_body(400,'missing username/password'); return
exec_sql('INSERT OR IGNORE INTO users(id,username,display_name,role,status,password_hash,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)',(uid('user'),username,display,'user','pending',hash_pw(pw),now(),now()))
approval(username,'user_registration',{'username':username},'low'); self.send_body(200,layout('Register Submitted','<p>注册已提交,等待审核。</p>',None)); return
user=self.user()
if not user: self.json({'error':'not_authenticated'},401); return
data={}; n=int(self.headers.get('Content-Length','0') or 0)
if n:
try: data=json.loads(self.rfile.read(n).decode() or '{}')
except Exception: data={}
actor=user['username']; role=user['role']
try:
if path=='/api/nodes/health' and require(user,['owner','admin']): exec_sql('UPDATE node_status SET health=?,last_seen_at=?,note=? WHERE node_id=?',(data.get('health'),now(),f'updated_by={actor}',data.get('node_id'))); audit(actor,'node_health_update',data.get('node_id'),data); self.json({'status':'ok'}); return
if path=='/api/routing/request' and require(user,['owner','admin']):
if data.get('role') not in ROLES or data.get('active_node') not in NODES: raise ValueError('bad routing')
if role=='owner': exec_sql('UPDATE routing_policy SET active_node=?,updated_at=? WHERE role=?',(data['active_node'],now(),data['role'])); audit(actor,'routing_changed','routing_policy',data); self.json({'status':'executed'}); return
self.json(approval(actor,'routing_change',data,'high')); return
if path=='/api/failover/dry-run' and require(user,['owner','admin']): self.json({'status':'dry_run','migration_order':rows('SELECT role,active_node,backup_node,failover_priority FROM routing_policy ORDER BY failover_priority')}); return
if path in ['/api/failover/request','/api/failover/request-failback'] and require(user,['owner','admin']): self.json(approval(actor,'failover_execute' if path.endswith('request') else 'failback_execute',data,'critical')); return
if path=='/api/approvals/approve' and require(user,['owner']):
ap=one('SELECT * FROM approvals WHERE id=?',(data.get('approval_id'),)); payload=jload(ap['payload']) if ap else {}
if not ap: raise ValueError('approval not found')
result={'approved':True}
if ap['action']=='routing_change': exec_sql('UPDATE routing_policy SET active_node=?,updated_at=? WHERE role=?',(payload.get('active_node'),now(),payload.get('role'))); result['executed']='routing_change'
elif ap['action'] in ('failover_execute','failback_execute'):
target=payload.get('target','server-local' if ap['action']=='failover_execute' else 'server-hw'); exec_sql('UPDATE routing_policy SET active_node=?,updated_at=?',(target,now())); result={'executed':ap['action'],'target':target}
elif ap['action']=='user_registration': exec_sql('UPDATE users SET status=?,updated_at=? WHERE username=?',('approved',now(),payload.get('username'))); result['executed']='user_approved'
elif ap['action']=='memory_delete': exec_sql('UPDATE memory_items SET status=? WHERE username=?',('soft_deleted',payload.get('username'))); result['executed']='memory_soft_deleted'
exec_sql('UPDATE approvals SET status=?,decided_by=?,decided_at=?,result=? WHERE id=?',('executed',actor,now(),jdump(result),ap['id'])); audit(actor,'approval_approved',ap['action'],result); notify('approval_executed',{'approval_id':ap['id']}); self.json({'status':'executed','result':result}); return
if path=='/api/approvals/reject' and require(user,['owner']): exec_sql('UPDATE approvals SET status=?,decided_by=?,decided_at=?,result=? WHERE id=?',('rejected',actor,now(),jdump({'rejected':True}),data.get('approval_id'))); audit(actor,'approval_rejected','approvals',data); self.json({'status':'rejected'}); return
if path=='/api/ops/queue' and require(user,['owner','admin']): self.json(queue_op(actor,data.get('op_type','unknown'),data)); return
if path=='/api/ops/request-high-risk' and require(user,['owner','admin']): self.json(approval(actor,data.get('op_type','unknown'),data,'critical')); return
if path=='/api/users/update' and require(user,['owner','admin']):
if data.get('role')=='owner' and role!='owner': raise PermissionError('only owner can grant owner')
if data.get('role') not in ROLES: raise ValueError('bad role')
exec_sql('UPDATE users SET role=?,status=?,updated_at=? WHERE username=?',(data.get('role'),data.get('status'),now(),data.get('username'))); audit(actor,'user_update',data.get('username'),data); self.json({'status':'updated'}); return
if path=='/api/quota/set' and require(user,['owner','admin']): exec_sql('INSERT INTO quota_policy(username,daily_limit,used_today,updated_at) VALUES(?,?,0,?) ON CONFLICT(username) DO UPDATE SET daily_limit=excluded.daily_limit,updated_at=excluded.updated_at',(data.get('username'),int(data.get('daily_limit',0)),now())); audit(actor,'quota_set',data.get('username'),data); self.json({'status':'updated'}); return
if path=='/api/models/request' and require(user,['owner','admin']): self.json(approval(actor,'model_policy_change',data,'high')); return
if path=='/api/memory/export' and require(user,['owner','admin']): self.json(queue_op(actor,'memory_export',data)); return
if path=='/api/memory/compress' and require(user,['owner','admin']): self.json(queue_op(actor,'memory_compress',data)); return
if path=='/api/memory/request-delete' and require(user,['owner','admin']): self.json(approval(actor,'memory_delete',data,'high')); return
if path=='/api/simulation/wechat' and require(user,['owner','admin']): self.json(simulate_wechat(data.get('username','sample-user'),data.get('message_type','text'),data.get('content',''))); return
if path=='/api/simulation/run-suite' and require(user,['owner','admin']): self.json(run_uat()); return
if path=='/api/life/diagnose' and require(user,['owner','admin']): self.json(life_diagnose_v5(actor)); return
if path=='/api/life/plan' and require(user,['owner','admin']): self.json(life_plan_v5(actor)); return
if path=='/api/life/request-apply' and require(user,['owner','admin']): self.json(approval(actor,'apply_life_plan',data,'high')); return
if path=='/api/patches/submit' and require(user,['owner','admin']): self.json(submit_patch_item(actor,data.get('patch_type','doc'),data.get('title','untitled'),data.get('target_area','docs'),data.get('content',''),data.get('risk','medium'))); return
if path=='/api/patches/request-apply' and require(user,['owner','admin']): self.json(approval(actor,'patch_apply',data,'high')); return
if path=='/api/versions/create' and require(user,['owner','admin']):
vid=uid('ver'); exec_sql('INSERT INTO baseline_versions(id,version_name,package_name,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(vid,data.get('version_name','v5-draft'),data.get('package_name','current'),data.get('summary','created from WebOps'),'draft',actor,now())); audit(actor,'version_created','baseline_versions',data); self.json({'version_id':vid}); return
if path=='/api/versions/request-publish' and require(user,['owner','admin']): self.json(approval(actor,'baseline_publish',data,'critical')); return
if path=='/api/optimization/diagnose' and require(user,['owner','admin']): self.json(diagnose()); return
if path=='/api/optimization/apply-request' and require(user,['owner','admin']): self.json(approval(actor,'apply_self_optimization_fix',data,'high')); return
if path=='/api/uat/run' and require(user,['owner','admin']): self.json(run_uat()); return
self.json({'error':'forbidden_or_unknown_endpoint','path':path},403)
except Exception as e:
audit(actor,'api_error',path,{'error':str(e),'data':data}); self.json({'error':str(e)},500)
def main():
init_db(); print(f'AIOS WebOps listening on {HOST}:{PORT} for {WEBOPS_DOMAIN}', flush=True); ThreadingHTTPServer((HOST,PORT),H).serve_forever()
if __name__=='__main__': main()
@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS users(id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, display_name TEXT, role TEXT NOT NULL CHECK(role IN ('owner','admin','user')), status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','disabled')), password_hash TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS node_status(node_id TEXT PRIMARY KEY CHECK(node_id IN ('server-hw','server-local')), health TEXT NOT NULL DEFAULT 'unknown', active INTEGER NOT NULL DEFAULT 0, last_seen_at TEXT, score INTEGER NOT NULL DEFAULT 0, note TEXT);
CREATE TABLE IF NOT EXISTS routing_policy(role TEXT PRIMARY KEY CHECK(role IN ('owner','admin','user')), primary_node TEXT NOT NULL CHECK(primary_node IN ('server-hw','server-local')), backup_node TEXT NOT NULL CHECK(backup_node IN ('server-hw','server-local')), active_node TEXT NOT NULL CHECK(active_node IN ('server-hw','server-local')), failover_priority INTEGER NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS approvals(id TEXT PRIMARY KEY, action TEXT NOT NULL, payload TEXT NOT NULL, requested_by TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected','executed','failed')), risk TEXT NOT NULL DEFAULT 'medium', created_at TEXT NOT NULL, decided_by TEXT, decided_at TEXT, result TEXT);
CREATE TABLE IF NOT EXISTS operations(id TEXT PRIMARY KEY, op_type TEXT NOT NULL, payload TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'queued' CHECK(status IN ('queued','running','succeeded','failed','skipped')), created_by TEXT NOT NULL, created_at TEXT NOT NULL, started_at TEXT, finished_at TEXT, result TEXT);
CREATE TABLE IF NOT EXISTS audit_logs(id TEXT PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, target TEXT, payload TEXT, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS events(id TEXT PRIMARY KEY, channel TEXT NOT NULL, actor TEXT, event_type TEXT NOT NULL, payload TEXT NOT NULL, route_target TEXT, status TEXT NOT NULL DEFAULT 'received', created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS quota_policy(username TEXT PRIMARY KEY, daily_limit INTEGER NOT NULL DEFAULT 100, used_today INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS model_policy(role TEXT PRIMARY KEY CHECK(role IN ('owner','admin','user')), default_model TEXT NOT NULL, allowed_models TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS memory_items(id TEXT PRIMARY KEY, username TEXT NOT NULL, scope TEXT NOT NULL DEFAULT 'user', content TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS owner_notifications(id TEXT PRIMARY KEY, event_type TEXT NOT NULL, payload TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'queued', created_at TEXT NOT NULL, sent_at TEXT);
CREATE TABLE IF NOT EXISTS self_optimization_suggestions(id TEXT PRIMARY KEY, category TEXT NOT NULL, severity TEXT NOT NULL, title TEXT NOT NULL, detail TEXT NOT NULL, proposed_fix TEXT, safe_auto_fix INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'open', created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS uat_results(id TEXT PRIMARY KEY, run_id TEXT NOT NULL, tc_id TEXT NOT NULL, category TEXT NOT NULL, status TEXT NOT NULL, latency_ms INTEGER, detail TEXT, created_at TEXT NOT NULL);
@@ -0,0 +1,5 @@
INSERT OR IGNORE INTO node_status(node_id, health, active, score, last_seen_at, note) VALUES ('server-hw','ok',1,100,datetime('now'),'primary logical node'),('server-local','ok',0,95,datetime('now'),'backup logical node');
INSERT OR IGNORE INTO routing_policy(role, primary_node, backup_node, active_node, failover_priority, updated_at) VALUES ('owner','server-hw','server-local','server-hw',1,datetime('now')),('admin','server-hw','server-local','server-hw',2,datetime('now')),('user','server-hw','server-local','server-hw',3,datetime('now'));
INSERT OR IGNORE INTO users(id, username, display_name, role, status, password_hash, created_at, updated_at) VALUES ('bootstrap-owner','owner','Bootstrap Owner','owner','approved','bootstrap-change-me',datetime('now'),datetime('now')),('bootstrap-admin','admin','Bootstrap Admin','admin','approved','bootstrap-change-me',datetime('now'),datetime('now')),('sample-user','sample-user','Sample User','user','approved','bootstrap-change-me',datetime('now'),datetime('now'));
INSERT OR IGNORE INTO model_policy(role, default_model, allowed_models, updated_at) VALUES ('owner','deepseekv4-flash','["deepseekv4-flash","qwen3.6-35b","gpt-5.5"]',datetime('now')),('admin','deepseekv4-flash','["deepseekv4-flash","qwen3.6-35b"]',datetime('now')),('user','deepseekv4-flash','["deepseekv4-flash"]',datetime('now'));
INSERT OR IGNORE INTO quota_policy(username, daily_limit, used_today, updated_at) VALUES ('owner',10000,0,datetime('now')),('admin',2000,0,datetime('now')),('sample-user',200,0,datetime('now'));
@@ -0,0 +1,43 @@
CREATE TABLE IF NOT EXISTS life_signals(
id TEXT PRIMARY KEY,
category TEXT NOT NULL,
severity TEXT NOT NULL,
title TEXT NOT NULL,
detail TEXT NOT NULL,
source TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'open',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS life_plans(
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
detail TEXT NOT NULL,
linked_signal_ids TEXT NOT NULL,
proposed_actions TEXT NOT NULL,
risk TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft',
created_by TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS patch_queue(
id TEXT PRIMARY KEY,
patch_type TEXT NOT NULL,
title TEXT NOT NULL,
target_area TEXT NOT NULL,
content TEXT NOT NULL,
submitted_by TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft',
risk TEXT NOT NULL DEFAULT 'medium',
created_at TEXT NOT NULL,
applied_at TEXT,
result TEXT
);
CREATE TABLE IF NOT EXISTS baseline_versions(
id TEXT PRIMARY KEY,
version_name TEXT NOT NULL,
package_name TEXT,
summary TEXT,
status TEXT NOT NULL DEFAULT 'draft',
created_by TEXT NOT NULL,
created_at TEXT NOT NULL
);
@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS gitops_baseline(
id TEXT PRIMARY KEY,
active_version TEXT NOT NULL,
active_tag TEXT NOT NULL,
source_package TEXT,
entry TEXT NOT NULL,
update_mode TEXT NOT NULL DEFAULT 'git_patch_only',
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS gitops_versions(version TEXT PRIMARY KEY, tag TEXT NOT NULL, level TEXT NOT NULL CHECK(level IN ('V','R','C','baseline')), summary TEXT, status TEXT NOT NULL DEFAULT 'draft', created_by TEXT NOT NULL DEFAULT 'system', created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS gitops_patches(id TEXT PRIMARY KEY, patch_name TEXT NOT NULL, branch TEXT NOT NULL, base_ref TEXT NOT NULL DEFAULT 'main', target_version TEXT, risk TEXT NOT NULL DEFAULT 'medium', status TEXT NOT NULL DEFAULT 'draft', created_by TEXT NOT NULL, created_at TEXT NOT NULL, applied_at TEXT, result TEXT);
CREATE TABLE IF NOT EXISTS llm_providers(id TEXT PRIMARY KEY, owner_username TEXT NOT NULL, name TEXT NOT NULL, provider_type TEXT NOT NULL DEFAULT 'openai_compatible', base_url TEXT NOT NULL, auth_mode TEXT NOT NULL DEFAULT 'bearer', api_key_secret_ref TEXT, scope TEXT NOT NULL DEFAULT 'user' CHECK(scope IN ('global','role','user')), status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS llm_models(id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, owner_username TEXT NOT NULL, model_name TEXT NOT NULL, display_name TEXT, context_window INTEGER DEFAULT 0, input_modalities TEXT NOT NULL DEFAULT '["text"]', output_modalities TEXT NOT NULL DEFAULT '["text"]', supports_tools INTEGER NOT NULL DEFAULT 0, supports_vision INTEGER NOT NULL DEFAULT 0, supports_reasoning INTEGER NOT NULL DEFAULT 0, cost_tier TEXT NOT NULL DEFAULT 'custom', is_base INTEGER NOT NULL DEFAULT 0, scope TEXT NOT NULL DEFAULT 'user' CHECK(scope IN ('global','role','user')), status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(owner_username, provider_id, model_name));
CREATE TABLE IF NOT EXISTS user_model_priority(username TEXT NOT NULL, model_id TEXT NOT NULL, priority INTEGER NOT NULL DEFAULT 100, enabled INTEGER NOT NULL DEFAULT 1, fallback_policy TEXT NOT NULL DEFAULT 'try_next_on_error', updated_at TEXT NOT NULL, PRIMARY KEY(username, model_id));
CREATE TABLE IF NOT EXISTS model_route_decisions(id TEXT PRIMARY KEY, username TEXT NOT NULL, task_type TEXT NOT NULL, selected_model_id TEXT, candidate_order TEXT NOT NULL, reason TEXT NOT NULL, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS user_runtime_profiles(username TEXT PRIMARY KEY, role TEXT NOT NULL DEFAULT 'user', tier TEXT NOT NULL DEFAULT 'regular', runtime_mode TEXT NOT NULL DEFAULT 'shared', allow_dedicated INTEGER NOT NULL DEFAULT 0, allow_custom_llm INTEGER NOT NULL DEFAULT 1, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS runtime_instances(id TEXT PRIMARY KEY, username TEXT NOT NULL, runtime_type TEXT NOT NULL, backend TEXT NOT NULL DEFAULT 'hermes', status TEXT NOT NULL DEFAULT 'registered', endpoint TEXT, last_seen_at TEXT, created_at TEXT NOT NULL, metadata TEXT NOT NULL DEFAULT '{}');
CREATE TABLE IF NOT EXISTS model_audit_logs(id TEXT PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, target TEXT, payload TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL);
INSERT OR IGNORE INTO gitops_baseline(id,active_version,active_tag,source_package,entry,update_mode,status,created_at,updated_at) VALUES('current','V1R1C0','V1R1C0','AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706','https://cnnetmkt.top','git_patch_only','active',datetime('now'),datetime('now'));
INSERT OR IGNORE INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES('V1R1C0','V1R1C0','R','Add MultiUser runtime selector, GitOps online baseline and user custom LLM priority registry.','ready','system',datetime('now'));
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env python3
from __future__ import annotations
import json, os, re, sqlite3, subprocess, time, uuid, hashlib, urllib.parse, html
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
WORKSPACE = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
DB_PATH = Path(os.getenv('AIOS_GITOPS_DB', str(ROOT / 'runtime' / 'gitops_model.sqlite3')))
SECRETS_DIR = Path(os.getenv('AIOS_GITOPS_SECRETS_DIR', str(ROOT / 'runtime' / 'secrets')))
TOKEN_FILE = Path(os.getenv('AIOS_GITOPS_TOKEN_FILE', str(SECRETS_DIR / 'gitops.tokens')))
HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
ENTRY = os.getenv('AIOS_ENTRY', 'https://cnnetmkt.top')
DEV_ALLOW = os.getenv('AIOS_GITOPS_DEV_ALLOW', '0') == '1'
BASE_MODELS = [
{'id':'base.deepseekv4-flash','provider':'one-api','model':'deepseekv4-flash','display':'DeepSeek V4 Flash','tier':'regular','reasoning':1,'vision':0,'tools':1,'context':128000},
{'id':'base.qwen3.6-35b','provider':'one-api','model':'qwen3.6-35b','display':'Qwen 3.6 35B','tier':'pro','reasoning':1,'vision':0,'tools':1,'context':262144},
{'id':'base.gpt-5.5','provider':'openai','model':'gpt-5.5','display':'GPT-5.5','tier':'ultra','reasoning':1,'vision':1,'tools':1,'context':256000},
]
def now() -> str: return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
def uid(prefix='id') -> str: return f'{prefix}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
def esc(x: Any) -> str: return html.escape('' if x is None else str(x))
def safe_name(s: str) -> str: return re.sub(r'[^a-zA-Z0-9_.@-]+', '_', s or 'unknown')[:120]
def db():
DB_PATH.parent.mkdir(parents=True, exist_ok=True); con = sqlite3.connect(DB_PATH); con.row_factory = sqlite3.Row; return con
def rows(sql, args=()):
with db() as con: return [dict(r) for r in con.execute(sql, args).fetchall()]
def one(sql, args=()):
r = rows(sql, args); return r[0] if r else None
def exec_sql(sql, args=()):
with db() as con: con.execute(sql, args); con.commit()
def audit(actor, action, target='', payload=None): exec_sql('INSERT INTO model_audit_logs(id,actor,action,target,payload,created_at) VALUES(?,?,?,?,?,?)',(uid('audit'), actor, action, target, json.dumps(payload or {}, ensure_ascii=False), now()))
def init_db():
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
schema = Path(__file__).resolve().parents[1] / 'migrations' / '004_gitops_custom_llm_runtime.sql'
with db() as con:
if schema.exists(): con.executescript(schema.read_text(encoding='utf-8'))
else:
con.executescript('''CREATE TABLE IF NOT EXISTS gitops_baseline(id TEXT PRIMARY KEY,active_version TEXT,active_tag TEXT,source_package TEXT,entry TEXT,update_mode TEXT,status TEXT,created_at TEXT,updated_at TEXT);CREATE TABLE IF NOT EXISTS gitops_versions(version TEXT PRIMARY KEY,tag TEXT,level TEXT,summary TEXT,status TEXT,created_by TEXT,created_at TEXT);CREATE TABLE IF NOT EXISTS gitops_patches(id TEXT PRIMARY KEY,patch_name TEXT,branch TEXT,base_ref TEXT,target_version TEXT,risk TEXT,status TEXT,created_by TEXT,created_at TEXT,applied_at TEXT,result TEXT);CREATE TABLE IF NOT EXISTS llm_providers(id TEXT PRIMARY KEY,owner_username TEXT,name TEXT,provider_type TEXT,base_url TEXT,auth_mode TEXT,api_key_secret_ref TEXT,scope TEXT,status TEXT,created_at TEXT,updated_at TEXT);CREATE TABLE IF NOT EXISTS llm_models(id TEXT PRIMARY KEY,provider_id TEXT,owner_username TEXT,model_name TEXT,display_name TEXT,context_window INTEGER,input_modalities TEXT,output_modalities TEXT,supports_tools INTEGER,supports_vision INTEGER,supports_reasoning INTEGER,cost_tier TEXT,is_base INTEGER,scope TEXT,status TEXT,created_at TEXT,updated_at TEXT, UNIQUE(owner_username, provider_id, model_name));CREATE TABLE IF NOT EXISTS user_model_priority(username TEXT,model_id TEXT,priority INTEGER,enabled INTEGER,fallback_policy TEXT,updated_at TEXT,PRIMARY KEY(username,model_id));CREATE TABLE IF NOT EXISTS model_route_decisions(id TEXT PRIMARY KEY,username TEXT,task_type TEXT,selected_model_id TEXT,candidate_order TEXT,reason TEXT,created_at TEXT);CREATE TABLE IF NOT EXISTS user_runtime_profiles(username TEXT PRIMARY KEY,role TEXT,tier TEXT,runtime_mode TEXT,allow_dedicated INTEGER,allow_custom_llm INTEGER,updated_at TEXT);CREATE TABLE IF NOT EXISTS runtime_instances(id TEXT PRIMARY KEY,username TEXT,runtime_type TEXT,backend TEXT,status TEXT,endpoint TEXT,last_seen_at TEXT,created_at TEXT,metadata TEXT);CREATE TABLE IF NOT EXISTS model_audit_logs(id TEXT PRIMARY KEY,actor TEXT,action TEXT,target TEXT,payload TEXT,created_at TEXT);''')
con.commit()
seed_base_models()
def seed_base_models():
exec_sql("INSERT OR IGNORE INTO llm_providers(id,owner_username,name,provider_type,base_url,auth_mode,api_key_secret_ref,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)", ('provider.one-api','system','AIOS One API','openai_compatible','http://127.0.0.1:13000/v1','bearer','secret://one-api/global','global','active',now(),now()))
exec_sql("INSERT OR IGNORE INTO llm_providers(id,owner_username,name,provider_type,base_url,auth_mode,api_key_secret_ref,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)", ('provider.openai','system','OpenAI Official','openai_compatible','https://api.openai.com/v1','bearer','secret://openai/global','global','active',now(),now()))
for m in BASE_MODELS:
pid = 'provider.openai' if m['provider']=='openai' else 'provider.one-api'
exec_sql('''INSERT OR IGNORE INTO llm_models(id,provider_id,owner_username,model_name,display_name,context_window,input_modalities,output_modalities,supports_tools,supports_vision,supports_reasoning,cost_tier,is_base,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', (m['id'], pid, 'system', m['model'], m['display'], m['context'], '["text"]', '["text"]', m['tools'], m['vision'], m['reasoning'], m['tier'], 1, 'global', 'active', now(), now()))
if not one('SELECT * FROM gitops_baseline WHERE id=?', ('current',)):
exec_sql('INSERT INTO gitops_baseline(id,active_version,active_tag,source_package,entry,update_mode,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)', ('current','V1R1C0','V1R1C0','AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706',ENTRY,'git_patch_only','active',now(),now()))
if not one('SELECT * FROM gitops_versions WHERE version=?', ('V1R1C0',)):
exec_sql('INSERT INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)', ('V1R1C0','V1R1C0','R','MultiUser runtime selector + GitOps + custom LLM priority registry','ready','system',now()))
def valid_token(token: str) -> bool:
if DEV_ALLOW: return True
if not token or not TOKEN_FILE.exists(): return False
return token.strip() in [x.strip() for x in TOKEN_FILE.read_text().splitlines() if x.strip()]
def git_cmd(args, check=False):
WORKSPACE.mkdir(parents=True, exist_ok=True); p = subprocess.run(['git'] + args, cwd=str(WORKSPACE), text=True, capture_output=True)
if check and p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or 'git command failed')
return {'code': p.returncode, 'stdout': p.stdout.strip(), 'stderr': p.stderr.strip(), 'cmd': ['git']+args}
def model_visible_to(username: str): return rows('''SELECT m.*, p.name provider_name, p.base_url, p.provider_type, p.scope provider_scope FROM llm_models m JOIN llm_providers p ON p.id=m.provider_id WHERE m.status='active' AND (m.scope='global' OR m.owner_username=? OR p.owner_username=?) ORDER BY m.is_base DESC, m.created_at ASC''', (username, username))
def priority_for(username: str): return rows('''SELECT pr.username, pr.model_id, pr.priority, pr.enabled, pr.fallback_policy, pr.updated_at, m.model_name, m.display_name, m.is_base, m.owner_username, p.base_url, p.name provider_name FROM user_model_priority pr JOIN llm_models m ON m.id=pr.model_id JOIN llm_providers p ON p.id=m.provider_id WHERE pr.username=? AND m.status='active' ORDER BY pr.priority ASC''', (username,))
def route_model(username: str, task_type='chat'):
ordered = [r for r in priority_for(username) if int(r.get('enabled') or 0) == 1]
if not ordered:
ordered = [m for m in model_visible_to(username) if int(m.get('is_base') or 0) == 1]
selected = dict(ordered[0]) if ordered else None
if selected and 'id' not in selected and selected.get('model_id'):
selected['id'] = selected.get('model_id')
reason = 'user_priority' if priority_for(username) else 'role_default_base_model'
candidate_ids = [(x.get('id') or x.get('model_id')) for x in ordered]
selected_id = selected.get('id') if selected else None
exec_sql('INSERT INTO model_route_decisions(id,username,task_type,selected_model_id,candidate_order,reason,created_at) VALUES(?,?,?,?,?,?,?)',(uid('route'), username, task_type, selected_id, json.dumps(candidate_ids, ensure_ascii=False), reason, now()))
return {'username': username, 'task_type': task_type, 'selected': selected, 'candidate_order': ordered, 'reason': reason}
def parse_body(handler):
n = int(handler.headers.get('Content-Length','0') or '0')
if n <= 0: return {}
raw = handler.rfile.read(n).decode('utf-8')
try: return json.loads(raw or '{}')
except Exception: return {'_raw': raw}
class Handler(BaseHTTPRequestHandler):
server_version = 'AIOSGitOpsModelAPI/V1R1C0'
def actor(self):
auth = self.headers.get('Authorization',''); token = auth.replace('Bearer ', '').strip() if auth.startswith('Bearer ') else self.headers.get('X-AIOS-Token','')
ok = valid_token(token); user = self.headers.get('X-AIOS-User') or ('dev-owner' if DEV_ALLOW else 'anonymous'); role = self.headers.get('X-AIOS-Role') or ('owner' if DEV_ALLOW else 'user')
return {'username': safe_name(user), 'role': role, 'token_ok': ok}
def require_write(self, roles=None):
a = self.actor()
if not a['token_ok']: self.send_json({'error':'unauthorized','hint':'write APIs require Bearer token or AIOS_GITOPS_DEV_ALLOW=1'}, 401); return None
if roles and a['role'] not in roles: self.send_json({'error':'forbidden','required_roles':roles,'role':a['role']}, 403); return None
return a
def send_json(self, data, code=200):
b = json.dumps(data, ensure_ascii=False, indent=2).encode('utf-8'); self.send_response(code); self.send_header('Content-Type','application/json; charset=utf-8'); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
def send_html(self, title, body, code=200):
links = '<a href="/base">base</a> <a href="/ver">ver</a> <a href="/diff">diff</a> <a href="/patch">patch</a> <a href="/run">run</a> <a href="/model">model</a> <a href="/api">api</a>'
html_doc = f'''<!doctype html><html><head><meta charset="utf-8"><title>{esc(title)}</title><style>body{{font-family:Arial,"Microsoft YaHei",sans-serif;margin:24px;background:#f8fafc}}a{{margin-right:12px}}.card{{background:white;border:1px solid #e5e7eb;border-radius:12px;padding:16px;margin:12px 0}}table{{border-collapse:collapse;width:100%;background:white}}td,th{{border:1px solid #ddd;padding:6px;text-align:left;vertical-align:top}}pre{{background:#0f172a;color:#e2e8f0;padding:12px;border-radius:8px;overflow:auto}}</style></head><body><h1>{esc(title)}</h1><div class="card">{links}</div>{body}</body></html>'''
b = html_doc.encode('utf-8'); self.send_response(code); self.send_header('Content-Type','text/html; charset=utf-8'); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
def table(self, data):
if not data: return '<p>No records.</p>'
cols = list(data[0].keys()); return '<table><thead><tr>'+''.join(f'<th>{esc(c)}</th>' for c in cols)+'</tr></thead><tbody>'+''.join('<tr>'+''.join(f'<td>{esc(r.get(c))}</td>' for c in cols)+'</tr>' for r in data)+'</tbody></table>'
def do_GET(self):
init_db(); u = urllib.parse.urlparse(self.path); path = u.path; q = urllib.parse.parse_qs(u.query); username = self.actor()['username']
try:
if path in ('/health','/api/health'): self.send_json({'status':'ok','version':'V1R1C0','entry':ENTRY,'workspace':str(WORKSPACE),'db':str(DB_PATH)}); return
if path in ('/api/status','/api'):
if path == '/api': self.send_html('AIOS V1R1C0 API', '<pre>'+esc(json.dumps(api_spec(),ensure_ascii=False,indent=2))+'</pre>'); return
self.send_json({'version':'V1R1C0','git':git_cmd(['status','--short']),'baseline':one('SELECT * FROM gitops_baseline WHERE id=?',('current',))}); return
if path in ('/api/base','/base'):
data = one('SELECT * FROM gitops_baseline WHERE id=?',('current',)) or {}; self.send_html('Baseline', '<pre>'+esc(json.dumps(data,ensure_ascii=False,indent=2))+'</pre>') if path == '/base' else self.send_json(data); return
if path in ('/api/ver','/ver'):
data = rows('SELECT * FROM gitops_versions ORDER BY created_at DESC'); self.send_html('Versions', self.table(data)) if path == '/ver' else self.send_json(data); return
if path in ('/api/patch','/patch'):
data = rows('SELECT * FROM gitops_patches ORDER BY created_at DESC'); self.send_html('Patches', self.table(data)) if path == '/patch' else self.send_json(data); return
if path in ('/api/diff','/diff'):
frm = (q.get('from') or q.get('from_') or ['HEAD~1'])[0]; to = (q.get('to') or ['HEAD'])[0]; diff = git_cmd(['diff', frm, to])
self.send_html('Diff', f'<p>from={esc(frm)} to={esc(to)}</p><pre>{esc(diff["stdout"] or diff["stderr"])}</pre>') if path == '/diff' else self.send_json({'from':frm,'to':to,'diff':diff}); return
if path in ('/api/model/base','/api/model/custom','/api/model/priority','/model'):
if path == '/api/model/base': self.send_json([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==1]); return
if path == '/api/model/custom': self.send_json([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==0]); return
if path == '/api/model/priority': self.send_json(priority_for(username)); return
body = '<h2>Base Models</h2>'+self.table([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==1]); body += '<h2>Custom Models</h2>'+self.table([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==0]); body += '<h2>Priority</h2>'+self.table(priority_for(username)); self.send_html('Model Registry', body); return
if path in ('/api/runtime/status','/run'):
data = rows('SELECT * FROM runtime_instances ORDER BY created_at DESC LIMIT 100'); self.send_html('Runtime', self.table(data)) if path == '/run' else self.send_json(data); return
if path == '/api/audit': self.send_json(rows('SELECT * FROM model_audit_logs ORDER BY created_at DESC LIMIT 200')); return
self.send_json({'error':'not_found','path':path},404)
except Exception as e: self.send_json({'error':str(e)},500)
def do_POST(self):
init_db(); path = urllib.parse.urlparse(self.path).path; data = parse_body(self)
try:
if path == '/api/model/custom/add':
a = self.require_write();
if not a: return
username = safe_name(data.get('username') or a['username']); provider_name = data.get('provider_name') or data.get('name') or 'custom-provider'; base_url = (data.get('base_url') or '').rstrip('/'); model_name = data.get('model_name') or data.get('model')
if not base_url or not model_name: self.send_json({'error':'base_url and model_name required'},400); return
secret_ref = data.get('api_key_secret_ref')
if data.get('api_key'):
sdir = SECRETS_DIR / 'llm' / safe_name(username); sdir.mkdir(parents=True, exist_ok=True); secret_id = uid('llmkey'); sp = sdir / f'{secret_id}.secret'; sp.write_text(str(data['api_key']), encoding='utf-8'); os.chmod(sp, 0o600); secret_ref = f'secret://llm/{safe_name(username)}/{secret_id}'
provider_id = data.get('provider_id') or f'provider.custom.{safe_name(username)}.{hashlib.sha1((provider_name+base_url).encode()).hexdigest()[:8]}'; model_id = data.get('model_id') or f'custom.{safe_name(username)}.{hashlib.sha1((provider_id+model_name).encode()).hexdigest()[:10]}'
exec_sql('''INSERT OR REPLACE INTO llm_providers(id,owner_username,name,provider_type,base_url,auth_mode,api_key_secret_ref,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)''',(provider_id, username, provider_name, data.get('provider_type','openai_compatible'), base_url, data.get('auth_mode','bearer'), secret_ref, data.get('scope','user'), 'active', now(), now()))
exec_sql('''INSERT OR REPLACE INTO llm_models(id,provider_id,owner_username,model_name,display_name,context_window,input_modalities,output_modalities,supports_tools,supports_vision,supports_reasoning,cost_tier,is_base,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',(model_id, provider_id, username, model_name, data.get('display_name') or model_name, int(data.get('context_window') or 0), json.dumps(data.get('input_modalities') or ['text'],ensure_ascii=False), json.dumps(data.get('output_modalities') or ['text'],ensure_ascii=False), int(bool(data.get('supports_tools',1))), int(bool(data.get('supports_vision',0))), int(bool(data.get('supports_reasoning',0))), data.get('cost_tier','custom'), 0, data.get('scope','user'), 'active', now(), now()))
if 'priority' in data: exec_sql('INSERT OR REPLACE INTO user_model_priority(username,model_id,priority,enabled,fallback_policy,updated_at) VALUES(?,?,?,?,?,?)',(username, model_id, int(data.get('priority')), int(bool(data.get('enabled',1))), data.get('fallback_policy','try_next_on_error'), now()))
audit(a['username'],'custom_model_add',model_id,{'username':username,'provider_id':provider_id,'model_name':model_name}); self.send_json({'status':'ok','provider_id':provider_id,'model_id':model_id,'secret_ref':secret_ref}); return
if path == '/api/model/custom/update':
a = self.require_write();
if not a: return
model_id = data.get('model_id'); m = one('SELECT * FROM llm_models WHERE id=?',(model_id,))
if not m: self.send_json({'error':'model not found'},404); return
if m['owner_username'] != a['username'] and a['role'] not in ('owner','admin'): self.send_json({'error':'forbidden'},403); return
fields=[]; args=[]
for col in ['display_name','status','cost_tier']:
if col in data: fields.append(f'{col}=?'); args.append(data[col])
for col in ['context_window','supports_tools','supports_vision','supports_reasoning']:
if col in data: fields.append(f'{col}=?'); args.append(int(bool(data[col])) if col.startswith('supports') else int(data[col]))
fields.append('updated_at=?'); args.append(now()); args.append(model_id); exec_sql(f'UPDATE llm_models SET {", ".join(fields)} WHERE id=?', args); audit(a['username'],'custom_model_update',model_id,data); self.send_json({'status':'updated','model_id':model_id}); return
if path == '/api/model/delete':
a = self.require_write();
if not a: return
model_id = data.get('model_id'); m = one('SELECT * FROM llm_models WHERE id=?',(model_id,))
if not m: self.send_json({'error':'model not found'},404); return
if int(m.get('is_base') or 0)==1: self.send_json({'error':'base model cannot be deleted'},400); return
if m['owner_username'] != a['username'] and a['role'] not in ('owner','admin'): self.send_json({'error':'forbidden'},403); return
exec_sql('UPDATE llm_models SET status=?,updated_at=? WHERE id=?',('disabled',now(),model_id)); audit(a['username'],'custom_model_delete',model_id,{}); self.send_json({'status':'disabled','model_id':model_id}); return
if path == '/api/model/priority/set':
a = self.require_write();
if not a: return
username = safe_name(data.get('username') or a['username'])
if username != a['username'] and a['role'] not in ('owner','admin'): self.send_json({'error':'forbidden'},403); return
items = data.get('items') or data.get('priority') or []; exec_sql('DELETE FROM user_model_priority WHERE username=?',(username,)); visible={m['id'] for m in model_visible_to(username)}
for idx,item in enumerate(items):
if isinstance(item,str): item={'model_id':item,'priority':(idx+1)*10,'enabled':1}
mid=item.get('model_id') or item.get('id')
if mid and mid in visible: exec_sql('INSERT OR REPLACE INTO user_model_priority(username,model_id,priority,enabled,fallback_policy,updated_at) VALUES(?,?,?,?,?,?)',(username, mid, int(item.get('priority',(idx+1)*10)), int(bool(item.get('enabled',1))), item.get('fallback_policy','try_next_on_error'), now()))
audit(a['username'],'model_priority_set',username,{'items':items}); self.send_json({'status':'updated','username':username,'priority':priority_for(username)}); return
if path == '/api/model/route/resolve': a=self.actor(); self.send_json(route_model(safe_name(data.get('username') or a['username']), data.get('task_type','chat'))); return
if path == '/api/model/test':
a = self.require_write();
if not a: return
mid=data.get('model_id'); m=one('SELECT m.*,p.base_url,p.provider_type FROM llm_models m JOIN llm_providers p ON p.id=m.provider_id WHERE m.id=?',(mid,))
if not m: self.send_json({'error':'model not found'},404); return
self.send_json({'status':'ok' if m['base_url'] and m['model_name'] else 'failed','live_test':os.getenv('AIOS_ENABLE_MODEL_LIVE_TEST','0')=='1','model':m['model_name'],'base_url':m['base_url'],'note':'Network live call is disabled unless AIOS_ENABLE_MODEL_LIVE_TEST=1'}); return
if path == '/api/runtime/select':
a=self.actor(); username=safe_name(data.get('username') or a['username']); role=data.get('role') or a['role']; tier=data.get('tier') or ('ultra' if role=='owner' else 'regular'); risk=data.get('risk','normal')
if risk in ('high_tool','shell','docker','write_prod'): runtime,backend='ephemeral_sandbox','openclaw_executor'
elif role in ('owner','admin') or tier=='ultra': runtime,backend='dedicated','hermes'
elif tier=='pro': runtime,backend='dedicated_or_shared','hermes'
else: runtime,backend='shared','hermes'
exec_sql('INSERT OR REPLACE INTO user_runtime_profiles(username,role,tier,runtime_mode,allow_dedicated,allow_custom_llm,updated_at) VALUES(?,?,?,?,?,?,?)',(username,role,tier,runtime,1 if runtime!='shared' else 0,1,now())); self.send_json({'username':username,'role':role,'tier':tier,'runtime_type':runtime,'backend':backend,'reason':'V1R1C0 selector policy'}); return
if path == '/api/runtime/register':
a = self.require_write(['owner','admin']);
if not a: return
rid=data.get('id') or uid('rt'); exec_sql('INSERT OR REPLACE INTO runtime_instances(id,username,runtime_type,backend,status,endpoint,last_seen_at,created_at,metadata) VALUES(?,?,?,?,?,?,?,?,?)',(rid,safe_name(data.get('username') or 'shared'),data.get('runtime_type','shared'),data.get('backend','hermes'),data.get('status','registered'),data.get('endpoint'),now(),now(),json.dumps(data.get('metadata') or {},ensure_ascii=False))); audit(a['username'],'runtime_register',rid,data); self.send_json({'status':'registered','id':rid}); return
if path == '/api/runtime/heartbeat': exec_sql('UPDATE runtime_instances SET status=?,last_seen_at=?,metadata=? WHERE id=?',(data.get('status','ok'),now(),json.dumps(data.get('metadata') or {},ensure_ascii=False),data.get('id'))); self.send_json({'status':'ok','id':data.get('id')}); return
if path == '/api/branch':
a = self.require_write(['owner','admin']);
if not a: return
branch=data.get('branch'); base=data.get('base','main')
if not branch: self.send_json({'error':'branch required'},400); return
git_cmd(['fetch','--all']); git_cmd(['checkout',base]); git_cmd(['checkout','-B',branch],check=True); audit(a['username'],'git_branch',branch,data); self.send_json({'status':'ok','branch':branch}); return
if path == '/api/file':
a = self.require_write(['owner','admin']);
if not a: return
branch=data.get('branch') or 'main'; rel=data.get('path'); content=data.get('content',''); msg=data.get('message') or f'chore(gitops): update {rel}'
if not rel or rel.startswith('/') or '..' in Path(rel).parts: self.send_json({'error':'invalid path'},400); return
git_cmd(['checkout',branch],check=True); target=WORKSPACE/rel; target.parent.mkdir(parents=True,exist_ok=True); target.write_text(content,encoding='utf-8'); git_cmd(['add',rel],check=True); commit=git_cmd(['commit','-m',msg]); audit(a['username'],'git_file_update',rel,{'branch':branch,'commit':commit}); self.send_json({'status':'committed','path':rel,'commit':commit}); return
if path == '/api/commit':
a = self.require_write(['owner','admin']);
if not a: return
msg=data.get('message') or 'chore(gitops): commit changes'; git_cmd(['add','.'],check=True); out=git_cmd(['commit','-m',msg]); audit(a['username'],'git_commit','workspace',{'message':msg}); self.send_json({'status':'ok','commit':out}); return
if path == '/api/tag':
a = self.require_write(['owner']);
if not a: return
ver=data.get('version') or data.get('tag')
if not re.match(r'^V\d+R\d+C\d+(-[a-zA-Z0-9_.-]+)?$', ver or ''): self.send_json({'error':'version must match VxRxCx'},400); return
msg=data.get('message') or f'release {ver}'; out=git_cmd(['tag','-a',ver,'-m',msg]);
if data.get('push',False): git_cmd(['push','origin',ver])
level='V' if re.match(r'^V[1-9]\d*R0C0', ver) else ('R' if re.match(r'^V\d+R[1-9]\d*C0', ver) else 'C'); exec_sql('INSERT OR REPLACE INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(ver,ver,level,msg,'ready',a['username'],now())); exec_sql('UPDATE gitops_baseline SET active_version=?,active_tag=?,updated_at=? WHERE id=?',(ver,ver,now(),'current')); audit(a['username'],'git_tag',ver,{'level':level}); self.send_json({'status':'tagged','version':ver,'git':out}); return
if path == '/api/rollback':
a = self.require_write(['owner','admin']);
if not a: return
target=data.get('target_version') or data.get('version')
if not target: self.send_json({'error':'target_version required'},400); return
out=git_cmd(['checkout',target]); audit(a['username'],'git_rollback',target,{}); self.send_json({'status':'rollback_checkout','target':target,'git':out}); return
if path == '/api/nl':
a = self.require_write();
if not a: return
text=data.get('text',''); intent='unknown'; plan=[]
if '回滚' in text or 'rollback' in text.lower(): intent='rollback'; plan=['extract target VxRxCx','call /api/rollback','run smoke test']
elif '对比' in text or 'diff' in text.lower(): intent='diff'; plan=['extract from/to refs','call /api/diff']
elif '模型' in text and ('优先' in text or 'priority' in text.lower()): intent='set_model_priority'; plan=['resolve model ids','call /api/model/priority/set']
elif '模型' in text and ('添加' in text or '新增' in text or 'add' in text.lower()): intent='add_custom_model'; plan=['validate provider','store api key as secret_ref','call /api/model/custom/add']
elif 'patch' in text.lower() or '补丁' in text: intent='create_patch'; plan=['create patch branch','commit patch metadata','run tests']
self.send_json({'intent':intent,'action_plan':plan,'execute':False,'safety_check':'V-level changes require owner approval'}); return
self.send_json({'error':'not_found','path':path},404)
except Exception as e: self.send_json({'error':str(e)},500)
def api_spec(): return {'version':'V1R1C0','short_links':['/base','/ver','/diff','/patch','/run','/model','/api'],'gitops_get':['/api/status','/api/base','/api/ver','/api/diff','/api/patch'],'gitops_post':['/api/branch','/api/file','/api/commit','/api/tag','/api/rollback','/api/nl'],'model_get':['/api/model/base','/api/model/custom','/api/model/priority'],'model_post':['/api/model/custom/add','/api/model/custom/update','/api/model/delete','/api/model/priority/set','/api/model/route/resolve','/api/model/test'],'runtime':['/api/runtime/status','/api/runtime/select','/api/runtime/register','/api/runtime/heartbeat'],'auth':'write APIs require Bearer token unless AIOS_GITOPS_DEV_ALLOW=1 for local acceptance tests'}
def main(): init_db(); print(f'AIOS V1R1C0 GitOps Model API listening on {HOST}:{PORT}'); ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
if __name__ == '__main__': main()
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
from __future__ import annotations
import json, re, sys
def parse(text: str):
t = text.lower(); version = re.findall(r'V\d+R\d+C\d+', text)
if '回滚' in text or 'rollback' in t: return {'intent':'rollback','target_version':version[0] if version else None,'risk':'high','requires_approval':True}
if '对比' in text or 'diff' in t: return {'intent':'diff','versions':version,'risk':'low','requires_approval':False}
if '添加' in text and '模型' in text: return {'intent':'add_custom_model','risk':'medium','requires_approval':False}
if '优先级' in text or 'priority' in t: return {'intent':'set_model_priority','risk':'medium','requires_approval':False}
if '发布' in text or 'release' in t:
level = 'V' if version and re.match(r'V\d+R0C0', version[0]) else ('R' if version and re.match(r'V\d+R\d+C0', version[0]) else 'C')
return {'intent':'release','version':version[0] if version else None,'change_level':level,'risk':'critical' if level=='V' else 'medium'}
return {'intent':'unknown','risk':'unknown','requires_approval':True}
if __name__ == '__main__': print(json.dumps(parse(' '.join(sys.argv[1:])), ensure_ascii=False, indent=2))
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
from __future__ import annotations
import json, os, sys, urllib.request
API = os.getenv('AIOS_GITOPS_API', 'http://127.0.0.1:18890/api')
TOKEN = os.getenv('AIOS_GITOPS_TOKEN', '')
def call(path, payload=None, method=None):
url = API + path; data = None if payload is None else json.dumps(payload, ensure_ascii=False).encode('utf-8')
req = urllib.request.Request(url, data=data, method=method or ('POST' if payload is not None else 'GET'))
if data: req.add_header('Content-Type','application/json')
if TOKEN: req.add_header('Authorization','Bearer '+TOKEN)
req.add_header('X-AIOS-User', os.getenv('AIOS_ACTOR','openclaw')); req.add_header('X-AIOS-Role', os.getenv('AIOS_ROLE','admin'))
with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read().decode('utf-8'))
def main(argv):
if len(argv) < 2: print('usage: openclaw_gitops_commands.py status|diff|release|rollback|model-priority|model-add ...'); return 2
cmd = argv[1]
if cmd == 'status': print(json.dumps(call('/status'), ensure_ascii=False, indent=2)); return 0
if cmd == 'diff': print(json.dumps(call('/diff?from='+argv[2]+'&to='+argv[3]), ensure_ascii=False, indent=2)); return 0
if cmd == 'release': print(json.dumps(call('/tag', {'version':argv[2], 'message':'release '+argv[2]}), ensure_ascii=False, indent=2)); return 0
if cmd == 'rollback': print(json.dumps(call('/rollback', {'target_version':argv[2]}), ensure_ascii=False, indent=2)); return 0
if cmd == 'model-add': print(json.dumps(call('/model/custom/add', {'provider_name':argv[2], 'base_url':argv[3], 'model_name':argv[4], 'priority':int(argv[5]) if len(argv)>5 else 10}), ensure_ascii=False, indent=2)); return 0
if cmd == 'model-priority': print(json.dumps(call('/model/priority/set', {'items': argv[2:]}), ensure_ascii=False, indent=2)); return 0
print('unknown command', cmd); return 2
if __name__ == '__main__': raise SystemExit(main(sys.argv))
@@ -0,0 +1 @@
<a href="/base">基线</a> <a href="/ver">版本</a> <a href="/diff">差异</a> <a href="/patch">补丁</a> <a href="/model">模型</a> <a href="/run">运行</a> <a href="/git">仓库</a>
+22
View File
@@ -0,0 +1,22 @@
{
"version": "V1R1C0",
"architecture_version": "V1",
"release_version": "R1",
"change_version": "C0",
"components": [
"webops",
"owner-admin-user",
"npm",
"casdoor",
"openclaw",
"hermes",
"one-api",
"gitops-control",
"baseline-registry",
"custom-llm-registry",
"runtime-selector"
],
"entry": "https://cnnetmkt.top",
"proxy": "npm",
"runtime_sync": "openclaw"
}
+28
View File
@@ -0,0 +1,28 @@
{
"active_version": "V1R1C0",
"active_tag": "V1R1C0-baseline-20260706",
"mode": "gitops-online-baseline",
"version_policy": "VxRxCx",
"update_mode": "git_patch_only",
"zip_update_deprecated": true,
"entry": "https://cnnetmkt.top",
"links": {
"git": "/git",
"baseline": "/base",
"versions": "/ver",
"diff": "/diff",
"patch": "/patch",
"model": "/model",
"runtime": "/run",
"api": "/api",
"hook": "/hook"
},
"features": {
"multiuser_runtime": true,
"shared_dedicated_runtime": true,
"custom_llm": true,
"user_model_priority": true,
"chatgpt_rw": true,
"codex_rw": true
}
}
+18
View File
@@ -0,0 +1,18 @@
digital_life:
name: AIOS v23 v5 Digital Life Core
entry: https://ops.cnnetmkt.top/life
loop:
- observe
- diagnose
- plan
- approve
- act
- learn
- rollback
safety:
high_risk_requires_owner_approval: true
production_delta_only: true
backup_before_change: true
rollback_on_failed_uat: true
chatgpt_can_write_package_assets: true
codex_deploys_to_runtime: true
+6
View File
@@ -0,0 +1,6 @@
AIOS_PUBLIC_DOMAIN=https://cnnetmkt.top
AIOS_WEBOPS_DOMAIN=https://ops.cnnetmkt.top
AIOS_API_DOMAIN=https://api.cnnetmkt.top
AIOS_WECHAT_CALLBACK_DOMAIN=https://wechat.cnnetmkt.top
AIOS_LOGICAL_PRIMARY_NODE=server-hw
AIOS_LOGICAL_BACKUP_NODE=server-local
+16
View File
@@ -0,0 +1,16 @@
{
"entry": "https://cnnetmkt.top",
"proxy": "NPM",
"short_links": {
"/git": "Gitea or existing Git Web UI",
"/base": "current GitOps baseline",
"/ver": "VxRxCx version list",
"/diff": "version diff view",
"/patch": "patch branch registry",
"/run": "runtime status and selector",
"/api": "GitOps + Model API",
"/model": "user custom LLM registry and priority UI",
"/hook": "Git webhook entry"
},
"notes": "Prefer NPM custom locations; do not overwrite raw nginx config."
}
+39
View File
@@ -0,0 +1,39 @@
{
"version": "V1R1C0",
"policy": {
"allow_user_custom_llm": true,
"api_key_storage": "secret_file_ref_only",
"base_models_locked_by": [
"owner",
"admin"
],
"user_can_order_priority": true,
"fallback_policy": "priority_order_then_role_default"
},
"base_models": [
{
"id": "base.deepseekv4-flash",
"provider": "one-api",
"model": "deepseekv4-flash",
"tier": "regular",
"reasoning": true,
"vision": false
},
{
"id": "base.qwen3.6-35b",
"provider": "one-api",
"model": "qwen3.6-35b",
"tier": "pro",
"reasoning": true,
"vision": false
},
{
"id": "base.gpt-5.5",
"provider": "openai",
"model": "gpt-5.5",
"tier": "ultra",
"reasoning": true,
"vision": true
}
]
}
+19
View File
@@ -0,0 +1,19 @@
{
"domain": "cnnetmkt.top",
"locations": {
"/git": "http://127.0.0.1:3000",
"/api": "http://127.0.0.1:18890/api",
"/base": "http://127.0.0.1:18890/base",
"/ver": "http://127.0.0.1:18890/ver",
"/diff": "http://127.0.0.1:18890/diff",
"/patch": "http://127.0.0.1:18890/patch",
"/run": "http://127.0.0.1:18890/run",
"/model": "http://127.0.0.1:18890/model",
"/hook": "http://127.0.0.1:18890/hook"
},
"ssl": {
"force_ssl": true,
"http2": true,
"websocket_support": true
}
}
+14
View File
@@ -0,0 +1,14 @@
roles:
owner:
priority: 1
description: 全局所有者,审批终点,可执行全部高危操作。
admin:
priority: 2
description: 日常管理员,可执行日常运维,高危操作进入 owner 审批。
user:
priority: 3
description: 普通用户,只能使用 AI 服务并查看自身信息。
legacy_role_mapping:
ultra: owner
pro: admin
regular: user
+22
View File
@@ -0,0 +1,22 @@
runtime:
role_model: owner_admin_user_only
logical_nodes:
primary: server-hw
backup: server-local
domains:
public_user: https://cnnetmkt.top
webops: https://ops.cnnetmkt.top
api: https://api.cnnetmkt.top
wechat_callback: https://wechat.cnnetmkt.top
one_person_ops:
avoid_new_components: true
production_core: reuse_current_deployment
webops_overlay_storage: sqlite
high_risk_actions_require_owner_approval:
- failover_execute
- failback_execute
- routing_change
- memory_delete
- model_policy_change
- rollback_release
- apply_self_optimization_fix
+51
View File
@@ -0,0 +1,51 @@
{
"version": "V1R1C0",
"rules": [
{
"match": {
"role": "owner"
},
"runtime": "dedicated",
"backend": "hermes"
},
{
"match": {
"role": "admin"
},
"runtime": "dedicated",
"backend": "hermes"
},
{
"match": {
"tier": "ultra"
},
"runtime": "dedicated",
"backend": "hermes"
},
{
"match": {
"tier": "pro"
},
"runtime": "dedicated_or_shared",
"backend": "hermes"
},
{
"match": {
"risk": "high_tool"
},
"runtime": "ephemeral_sandbox",
"backend": "openclaw_executor"
},
{
"match": {
"default": true
},
"runtime": "shared",
"backend": "hermes"
}
],
"security": {
"docker_sock_direct_mount": false,
"executor": "openclaw_or_restricted_docker_proxy"
}
}
+13
View File
@@ -0,0 +1,13 @@
uat_cases:
- WX-TEXT-001
- WX-TEXT-002
- WX-IMAGE-001
- WX-FILE-001
- WEB-REG-001
- WEB-LOGIN-001
- ADMIN-USERS-001
- ADMIN-QUOTA-001
- OWNER-APPROVAL-001
- OWNER-FAILOVER-001
- HA-001
- OPT-001
+22
View File
@@ -0,0 +1,22 @@
webops_pages:
- /dashboard
- /nodes
- /roles
- /routing
- /failover
- /approvals
- /operations
- /users
- /quota
- /models
- /memory
- /channels/wechat
- /life
- /patches
- /optimization
- /simulation
- /logs
- /config
- /uat
- /versions
- /evidence
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PKG_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
: "${AIOS_RUNTIME_ROOT:=$(pwd)}"
: "${AIOS_RELEASE_ID:=webops-full-refresh-$(date +%Y%m%dT%H%M%S)}"
export AIOS_RUNTIME_ROOT AIOS_RELEASE_ID
bash "$PKG_ROOT/scripts/backup_before_fix.sh"
python3 "$PKG_ROOT/fixes/patch_runtime_owner_admin_user.py" "$AIOS_RUNTIME_ROOT"
bash "$PKG_ROOT/scripts/install_webops_overlay.sh" "$AIOS_RUNTIME_ROOT"
python3 "$PKG_ROOT/scripts/apply_migrations.py" "$AIOS_RUNTIME_ROOT"
python3 "$PKG_ROOT/scripts/validate_roles_and_domains.py" "$AIOS_RUNTIME_ROOT"
python3 "$PKG_ROOT/scripts/validate_no_legacy_roles.py" "$AIOS_RUNTIME_ROOT" || true
bash "$PKG_ROOT/scripts/smoke_test.sh" "$AIOS_RUNTIME_ROOT"
python3 "$PKG_ROOT/scripts/generate_release_manifest.py" "$AIOS_RUNTIME_ROOT"
echo "DELTA_FIX_ONLY_APPLY_OK release=$AIOS_RELEASE_ID runtime=$AIOS_RUNTIME_ROOT"
python3 "$PKG/fixes/patch_runtime_v5_guards.py" "$TARGET"
echo "V5_DIGITAL_LIFE_DELTA_APPLIED"
+15
View File
@@ -0,0 +1,15 @@
# AIOS v23 全量刷新包范围说明
本包基于最新实际部署包生成,采用“生产只执行修正部分,全量包用于整体评估和验收”的方式。
## 两层交付
1. `delta-fix/`:生产执行层,只补齐 WebOps、角色收敛、审批、仿真、自优化、回滚入口。
2. `source-baseline/`:最新实际部署包原样保留,用于整体评估、对比、回滚和后续版本刷新。
## 不做的事
- 不全量替换当前生产系统。
- 不新增复杂组件栈。
- 不让自优化系统未经审批修改高风险生产配置。
- 不使用 `pro/ultra/regular` 作为新权限角色。
+32
View File
@@ -0,0 +1,32 @@
# AIOS v23 生产架构全量优化设计
## 目标形态
以 `https://ops.cnnetmkt.top` 为统一 WebOps 控制面,以 `https://cnnetmkt.top` 为用户入口,以 `server-hw/server-local` 为双节点执行抽象,以 `owner/admin/user` 为唯一角色体系。
## 核心域名
| 用途 | 域名 |
|---|---|
| 用户门户 / 注册 / 登录 | `https://cnnetmkt.top` |
| WebOps 控制台 | `https://ops.cnnetmkt.top` |
| API 入口 | `https://api.cnnetmkt.top` |
| 微信回调 / 适配 | `https://wechat.cnnetmkt.top` |
## 角色模型
- `owner`:全局所有者,审批终点,能执行全部高危操作。
- `admin`:日常管理员,可管理用户、额度、运维任务;高危动作进入 owner 审批。
- `user`:普通用户,只能使用 AI 服务、查看自身信息。
历史 `ultra/pro/regular` 只能作为归档旧配置识别,不能作为新运行角色。
## WebOps 闭环
```text
Web UI → API → 审批/任务 → 审计 → owner 微信通知 → 结果/证据 → 可回滚
```
## 一人运维约束
默认不新增 K8s、NATS、Kafka、Redis、Qdrant、Prometheus 全家桶。新增 WebOps 是单体 overlay,不破坏当前运行系统。
@@ -0,0 +1,67 @@
# 01 AIOS v23 v5 数字生命体架构设计
## 1. 系统定位
AIOS v23 v5 是 Web-first 的数字生命体运行系统。它不是让系统无限自治地修改生产,而是将“感知、诊断、计划、审批、执行、学习、回滚”形成安全闭环。
## 2. 核心分层
```text
WebOps UI
↓
Digital Life Core(感知/诊断/计划/建议)
↓
Aegis Approval Gate(owner 审批闸门)
↓
NEXUS Runtime Bridge(执行桥)
↓
AIOS Runtime / OpenClaw / Hermes / WeChat
```
## 3. 数字生命体闭环
| 阶段 | Web 页面 | 说明 |
|---|---|---|
| Observe | /dashboard /logs /simulation | 采集事件、日志、用户行为、节点状态 |
| Diagnose | /life /optimization | 发现路由异常、审批积压、节点退化、UAT 失败 |
| Plan | /life | 生成可审查执行计划 |
| Approve | /approvals | owner 审批高风险计划 |
| Act | /operations /patches | OpenClaw/Codex 执行修正脚本或补丁 |
| Learn | /versions /uat | 保存版本、验收结果和优化效果 |
| Rollback | /operations | 失败自动或人工回滚 |
## 4. 一人运维约束
- 不引入复杂组件作为默认依赖。
- 使用 SQLite + Python 标准库 WebOps 作为最小生产控制面。
- OpenClaw/Codex 执行复杂修复,WebOps 负责入口、审批、审计和证据。
- 每个高危操作必须可回滚。
## 5. 角色和权限
仅存在三类角色:owner、admin、user。
| 操作 | owner | admin | user |
|---|---|---|---|
| 审批 | 允许 | 不允许 | 不允许 |
| 用户管理 | 允许 | 允许,不能授予 owner | 仅自己 |
| 路由变更 | 直接执行 | 提交审批 | 不允许 |
| 故障切换 | 审批并执行 | 提交审批 | 不允许 |
| 补丁提交 | 允许 | 允许 | 不允许 |
| 补丁执行 | 审批后执行 | 提交审批 | 不允许 |
| 微信使用 | 允许 | 允许 | 允许 |
## 6. 节点模型
只使用 server-hw 和 server-local。默认 active_node 为 server-hw,backup_node 为 server-local。
故障迁移顺序:owner → admin → user。
回切顺序:user → admin → owner。
## 7. 域名
- 用户门户:`https://cnnetmkt.top`
- WebOps:`https://ops.cnnetmkt.top`
- API:`https://api.cnnetmkt.top`
- 微信回调:`https://wechat.cnnetmkt.top`
- 数字生命体:`https://ops.cnnetmkt.top/life`
@@ -0,0 +1,16 @@
# AIOS v23 用户需求文档(owner/admin/user)
| 角色 | 需求 |
|---|---|
| owner | 通过 WebOps 看到全局状态,审批高危动作,接收个人微信告警,必要时一键回滚。 |
| admin | 通过 WebOps 完成日常用户、额度、模型、UAT、仿真、日志处理,不能绕过 owner 审批。 |
| user | 通过微信和 Web 正常使用 AI,支持文本、图片、文件,不看到内部错误字段。 |
## user 需求
- 微信发送普通文字,必须正常回复。
- 微信引用普通文字,不得误入图片路线。
- 微信发送图片,进入 Vision。
- 微信引用 PDF/Word/Excel/TXT/CSV/ZIP 等文件,不得进入 Vision。
- 用户 Web 注册、登录、查看自身额度和绑定状态。
- 不得看到 `VISION_ROUTE_TARGET`、`FAIL_REASON`、Traceback 等内部字段。
+37
View File
@@ -0,0 +1,37 @@
# Codex / OpenClaw 生产部署执行要求
## 执行入口
```bash
bash package_verify_full.sh
bash delta-fix/apply_delta_fix_only.sh
bash tests/run_acceptance_with_autofix.sh
```
## 多 Agent 并发分工
| Agent | 职责 |
|---|---|
| agent-backup | 执行备份,记录 manifest。 |
| agent-webops | 安装 WebOps overlay,验证页面和 API。 |
| agent-role-policy | 验证 owner/admin/user 收敛。 |
| agent-wechat-sim | 跑 simulated-wechat 文本/图片/文件/引用矩阵。 |
| agent-admin-web | 跑注册、登录、用户管理、审批、额度、模型 Web 操作测试。 |
| agent-selfopt | 对失败测试生成建议,尝试安全修复并复测。 |
| agent-reporter | 汇总证据、生成验收报告、通知 owner 微信。 |
## 执行顺序
1. 全包校验。
2. 当前运行系统备份。
3. 安装 WebOps overlay。
4. 应用 owner/admin/user 角色收敛配置。
5. 应用数据库迁移。
6. 启动 WebOps。
7. smoke test。
8. simulated-wechat 仿真。
9. Web 注册/登录/管理测试。
10. admin/owner 操作测试。
11. UAT with self-optimization。
12. 证据导出。
13. 生成新版本 manifest。
@@ -0,0 +1,53 @@
# 03 Codex / OpenClaw v5 生产部署执行指令
## 0. 任务角色
Codex 是部署总控,OpenClaw 可启用多个 agent 并行执行。ChatGPT 可读写本包文档、配置和补丁内容,Codex 负责实际部署、验证和回写基线。
## 1. 严格约束
- 角色只允许 owner/admin/user。
- 节点只允许 server-hw/server-local。
- 生产上只执行 `delta-fix/apply_delta_fix_only.sh`,不得无差别覆盖整个运行系统。
- 所有高危动作必须先备份,再审批,再执行,再验证。
- 验收失败必须运行自诊断和安全自修复;仍失败则回滚。
## 2. 推荐多 agent 分工
| Agent | 任务 |
|---|---|
| agent-backup | 执行备份,生成备份证据 |
| agent-webops | 部署 active-runtime WebOps 覆盖层 |
| agent-route-guard | 执行媒体/文件/文本路由保护修正 |
| agent-ui | 验证所有 Web UI 页面和按钮 |
| agent-uat | 执行微信仿真、注册登录、管理员操作、owner 审批测试 |
| agent-life-core | 执行数字生命体诊断、建议和安全自修复 |
| agent-reporter | 汇总结果,通知 owner 微信,写入新版本基线 |
## 3. 执行命令
```bash
bash package_verify_full.sh
bash scripts/silent_install_v5.sh
AIOS_RUNTIME_ROOT=<current-runtime-root> bash delta-fix/apply_delta_fix_only.sh
bash tests/run_v5_acceptance_with_autofix.sh
bash scripts/export_evidence.sh
```
## 4. 部署后 Web 地址
- 用户门户:`https://cnnetmkt.top`
- WebOps:`https://ops.cnnetmkt.top`
- API:`https://api.cnnetmkt.top`
- 微信回调:`https://wechat.cnnetmkt.top`
- 数字生命体:`https://ops.cnnetmkt.top/life`
## 5. 成功标准
- WebOps 页面全部可访问。
- owner/admin/user 权限符合矩阵。
- user 可注册、登录、微信收发。
- admin 可管理用户、额度和低风险运维。
- owner 可审批高风险动作。
- 微信文字、图片、文件引用路由正确。
- UAT 通过;失败项有自优化建议、修复尝试和可回滚证据。
+11
View File
@@ -0,0 +1,11 @@
# 只执行修正部分 Runbook
生产上只执行:
```bash
bash delta-fix/apply_delta_fix_only.sh
```
该脚本只做:备份、active role marker、WebOps overlay、SQLite 迁移、release manifest、smoke test。
不做:不覆盖历史文档、不解包替换 source-baseline、不修改真实用户数据、不全量重装系统。
+25
View File
@@ -0,0 +1,25 @@
# 04 WebOps UI v5 功能矩阵
| 页面 | 能看 | 能决策 | 能修改 | 审批 | 说明 |
|---|---|---|---|---|---|
| /dashboard | 是 | 是 | 否 | 否 | 总览域名、节点、路由、角色 |
| /nodes | 是 | 是 | 是 | 中高风险需审批 | 修改节点健康状态 |
| /roles | 是 | 否 | 否 | 否 | 只展示 owner/admin/user |
| /routing | 是 | 是 | 是 | admin 需 owner 审批 | 修改角色路由 active_node |
| /failover | 是 | 是 | 是 | 必须审批 | 预演、申请切换、申请回切 |
| /approvals | 是 | 是 | 是 | owner | 审批中心 |
| /operations | 是 | 是 | 是 | 高风险审批 | 备份、健康检查、证据、回滚 |
| /users | 是 | 是 | 是 | 授予 owner 仅 owner | 用户管理 |
| /quota | 是 | 是 | 是 | 按策略 | 额度管理 |
| /models | 是 | 是 | 申请 | 是 | 模型策略申请 |
| /memory | 是 | 是 | 软删除需审批 | 是 | 记忆导出、压缩、删除 |
| /channels/wechat | 是 | 是 | 是 | 否 | owner 微信通知队列 |
| /life | 是 | 是 | 申请 | 是 | 数字生命体诊断与计划 |
| /patches | 是 | 是 | 申请 | 是 | ChatGPT/Codex 补丁协作 |
| /optimization | 是 | 是 | 申请 | 是 | 自优化建议 |
| /simulation | 是 | 是 | 是 | 否 | 安全仿真,不触达真实用户 |
| /logs | 是 | 否 | 否 | 否 | 审计和事件 |
| /config | 是 | 否 | 否 | 否 | 域名、角色、节点 |
| /uat | 是 | 是 | 是 | 否 | 运行验收 |
| /versions | 是 | 是 | 发布需审批 | 是 | 版本记录和发布 |
| /evidence | 是 | 是 | 是 | 否 | 导出证据 |
+21
View File
@@ -0,0 +1,21 @@
# WebOps UI 功能矩阵
| 页面 | 能看 | 能决策 | 能修改 |
|---|---|---|---|
| Dashboard | 是 | 部分 | 否 |
| Nodes | 是 | owner/admin | 高危审批 |
| Roles | 是 | owner | owner |
| Routing | 是 | owner/admin | owner 或审批后 |
| Failover | 是 | owner/admin | owner 审批后 |
| Approvals | 是 | owner | owner |
| Operations | 是 | owner/admin | 高危审批 |
| Users | 是 | owner/admin | 边界受控 |
| Quota | 是 | owner/admin | owner/admin |
| Models | 是 | owner/admin | owner 审批后 |
| Memory | 是 | owner/admin | 删除需审批 |
| WeChat | 是 | owner/admin | 高危审批 |
| Optimization | 是 | owner/admin | owner 审批后 |
| Simulation | 是 | owner/admin | 只 simulated |
| Logs | 是 | 否 | 否 |
| Config | 是 | 否 | 否 |
| UAT | 是 | owner/admin | 只生成结果/建议 |
+43
View File
@@ -0,0 +1,43 @@
# 06 AIOS v23 v5 验收测试文档
## 1. 微信用户行为测试
| TC_ID | 输入 | 预期 |
|---|---|---|
| WX-TEXT-001 | user 发送普通文字 | 文本路线,正常回复 |
| WX-TEXT-002 | user 引用普通文字后回复 | 文本路线,不进入图片识别 |
| WX-TEXT-003 | “看一下这个方案是否合理” | 文本路线,不误判图片 |
| WX-IMAGE-001 | user 发送图片 | 图片路线 |
| WX-IMAGE-002 | user 引用图片追问 | 图片路线 |
| WX-FILE-001 | user 引用 PDF | 文件路线,不进入图片识别 |
| WX-FILE-002 | user 引用表格 | 文件路线,不进入图片识别 |
| WX-FILE-003 | user 引用文档 | 文件路线,不进入图片识别 |
| WX-ERR-001 | 缓存图片失效 | 友好提示,不显示内部字段 |
## 2. Web 用户行为测试
| TC_ID | 场景 | 预期 |
|---|---|---|
| WEB-REG-001 | user 注册 | 状态 pending,创建审批 |
| WEB-LOGIN-001 | approved 用户登录 | 进入 dashboard |
| ADMIN-USERS-001 | admin 修改 user 状态 | 成功,不能授予 owner |
| ADMIN-QUOTA-001 | admin 设置额度 | 成功并审计 |
| OWNER-APPROVAL-001 | owner 审批用户注册/高危操作 | 状态 executed |
## 3. 运维和自优化测试
| TC_ID | 场景 | 预期 |
|---|---|---|
| HA-001 | failover dry-run | 返回 owner/admin/user 迁移顺序 |
| LIFE-001 | 数字生命体诊断 | 生成 signal 和 suggestion |
| PATCH-001 | 提交补丁 | patch_queue 记录补丁 |
| ROLLBACK-001 | 申请回滚 | 进入 approval 或 operations |
## 4. 自动修复策略
验收失败时:
1. 运行 `self_diagnose_v5.py`。
2. 对安全项运行 `self_optimize_v5.py --safe-only`。
3. 重新运行 UAT。
4. 仍失败则标记阻塞并回滚。
+29
View File
@@ -0,0 +1,29 @@
# AIOS v23 全量验收测试文档
## 验收范围
- 微信用户行为:文字、图片、文件、引用、命令、异常。
- Web 用户行为:注册、登录、查看自身信息。
- admin Web 操作:审核、用户管理、额度、日志、UAT、仿真。
- owner Web 操作:审批、failover、routing、模型、记忆、回滚、自优化。
- HA:server-hw/server-local 健康、切换、回切、可回滚。
- 安全:脱敏、角色隔离、文件/图片/文字路线隔离。
## 核心用例
| TC_ID | 输入 | 预期 |
|---|---|---|
| WX-TEXT-001 | user 微信普通文字 | 文本路线,正常回复 |
| WX-TEXT-002 | user 引用普通文字 | 文本路线,不进 Vision |
| WX-IMAGE-001 | user 发送图片 | Vision 路线 |
| WX-FILE-001 | user 引用 PDF | 文件/文本路线,不进 Vision |
| WX-FILE-002 | user 引用 Excel | 文件/文本路线,不进 Vision |
| WEB-REG-001 | 访问注册页 | 创建 pending user |
| WEB-LOGIN-001 | approved user 登录 | 登录成功 |
| ADMIN-USERS-001 | admin 审核用户 | 审核成功并审计 |
| ADMIN-QUOTA-001 | admin 改 user 额度 | 生效并审计 |
| OWNER-APPROVAL-001 | owner 审批高危动作 | 审批成功并执行 |
| HA-001 | server-hw down | active_node 切换到 server-local |
| OPT-001 | UAT 失败 | 生成自优化建议 |
P0/P1 必须阻塞上线。
+40
View File
@@ -0,0 +1,40 @@
# 07 ChatGPT 与 Codex 协同规则
## 1. ChatGPT 权限
ChatGPT 允许在本包中读写:
- 文档
- 配置模板
- 补丁说明
- UAT 用例
- Patch Studio 补丁内容
ChatGPT 不直接在生产执行高危操作。
## 2. Codex 权限
Codex 负责:
- 执行静默安装
- 调用 OpenClaw 多 agent 并行部署
- 应用修正脚本
- 执行 UAT
- 回写基线
- 生成证据
## 3. 补丁流程
```text
ChatGPT 生成补丁/文档/配置
↓
/patches 提交 patch_queue
↓
Codex 读取并落地到工程文件
↓
owner 审批
↓
OpenClaw 执行
↓
UAT + 证据 + 可回滚
```
@@ -0,0 +1,9 @@
# 自优化与回滚设计
自进化大脑默认只做诊断、建议、白名单安全修复、复测、生成 owner 审批。不得自动删除用户数据、修改真实模型计费策略、重启核心生产服务、清空记忆或未审批 failover。
回滚入口:
```bash
bash scripts/rollback_last_delta.sh
```
+10
View File
@@ -0,0 +1,10 @@
# 域名与路由规划
| 入口 | 域名 |
|---|---|
| 用户门户 | `https://cnnetmkt.top` |
| WebOps | `https://ops.cnnetmkt.top` |
| API | `https://api.cnnetmkt.top` |
| 微信回调 | `https://wechat.cnnetmkt.top` |
所有配置和 UI 使用 `server-hw`、`server-local`。
+9
View File
@@ -0,0 +1,9 @@
# 基线保留策略
最新实际部署包原样保留在:
```text
source-baseline/current_actual_deployment_source.zip
```
任何 agent 不得修改该文件。生产只执行 `delta-fix/` 修正层。
@@ -0,0 +1,57 @@
# AIOS V1R1C0 全量架构设计:MultiUser Runtime + GitOps + Custom LLM
## 1. 版本定位
本版版本号:`V1R1C0`。按照 `VxRxCx` 规则,本版属于 **R 级功能版本升级**:在现有 V1 架构不整体推翻的前提下,增加多用户 runtime 选择、在线 GitOps 基线、自定义 LLM 模型注册与优先级控制。
## 2. 架构原则
1. **复用优先**:NPM 继续作为入口代理;Casdoor/WebOps 继续作为身份权限;OpenClaw 继续作为执行器;Hermes 继续作为规划与自然语言理解层。
2. **Git 为单一真相源**:ZIP 仅作为初始导入源,后续统一通过 branch/commit/tag/patch。
3. **模型密钥不进用户容器**:用户自定义 LLM 的 API Key 只保存为 secret_ref。
4. **shared/dedicated 混合 runtime**:Regular 默认 shared,Pro/Ultra/Owner/Codex/高危工具可 dedicated 或 sandbox。
5. **用户可控模型优先级**:用户可在基础模型之外添加自定义 LLM,并决定 fallback 顺序。
## 3. 目标逻辑架构
```text
NPM / cnnetmkt.top
├─ / WebOps Portal
├─ /git Git Web UI
├─ /base 当前基线
├─ /ver VxRxCx 版本
├─ /diff 版本差异
├─ /patch Patch 管理
├─ /model 模型注册与优先级
├─ /run Runtime 状态
└─ /api GitOps + Model + Runtime API
↓
WebOps / Casdoor 权限
↓
Hermes NL / Risk / Planner
↓
OpenClaw Executor
↓
Runtime Selector
↓
Model Router
```
## 4. 从 MultiUserClaw 借鉴但不照搬
| 借鉴点 | AIOS 落地方式 |
|---|---|
| Platform Gateway | 复用 WebOps/Router Gateway,不新增独立用户体系 |
| dedicated/shared runtime | Regular shared,Pro/Ultra/Owner dedicated,高危工具 sandbox |
| LLM key gateway 托管 | One API / Secret Store / GitOps API 保存 secret_ref |
| Agent 模板下发 | 纳入 WebOps Agent Template Registry |
| API token 调用 Agent | token 绑定 user_id、role、tier、scope、expire_at |
| 容器生命周期 | 由 OpenClaw Executor 或受限 Docker Proxy 管理,不让普通 Gateway 挂 docker.sock |
## 5. 数据面隔离
- 用户模型配置按 username 隔离。
- 用户 priority 只影响本人。
- 全局基础模型只允许 owner/admin 调整。
- 自定义模型 API Key 不写入 Git,不写入普通配置文件。
- runtime selector 不允许普通用户直接选择宿主机执行器。
+29
View File
@@ -0,0 +1,29 @@
# AIOS V1R1C0 用户需求规格说明
## Owner
- 查看全局基线版本、版本差异和发布历史。
- 审批 V 级架构变化、R 级高风险功能变化和高危 runtime 操作。
- 管理基础模型池和角色默认模型策略。
- 回滚到任意 VxRxCx 版本。
## Admin
- 创建和审核 patch branch。
- 管理 Regular/Pro/Ultra 用户策略。
- 运行 UAT 和 smoke test。
- 查看模型路由决策和失败原因。
## User
- 使用系统预置基础模型。
- 在基础模型之外添加自己的自定义 LLM。
- 设置模型优先级,例如:我的本地 qwen → deepseekv4-flash → qwen3.6-35b。
- 模型不可用时自动 fallback 到下一优先级模型。
## Codex / ChatGPT
- 通过 GitOps API 创建 branch、修改文件、提交 commit、生成 tag。
- 通过 NL API 解释自然语言指令。
- 使用 `/api/model/*` 接口管理模型注册与优先级。
- 所有写操作必须产生审计记录。
+79
View File
@@ -0,0 +1,79 @@
# AIOS V1R1C0 接口规格:GitOps + custom LLM + Runtime Selector
默认本地服务:`127.0.0.1:18890`。
## 1. 认证
写接口必须带:
```http
Authorization: Bearer <AIOS_GITOPS_TOKEN>
X-AIOS-User: <username>
X-AIOS-Role: owner|admin|user
```
## 2. GitOps 接口
| 方法 | 路径 | 功能 |
|---|---|---|
| GET | `/api/status` | 查看 GitOps 状态 |
| GET | `/api/base` | 当前 baseline |
| GET | `/api/ver` | 版本列表 |
| GET | `/api/diff?from=&to=` | 版本差异 |
| GET | `/api/patch` | Patch 列表 |
| POST | `/api/branch` | 创建分支 |
| POST | `/api/file` | 写文件并 commit |
| POST | `/api/commit` | 提交已有变更 |
| POST | `/api/tag` | 创建 VxRxCx tag |
| POST | `/api/rollback` | checkout 指定版本 |
| POST | `/api/nl` | 自然语言路由 |
## 3. custom LLM 接口
### 添加用户自定义模型
`POST /api/model/custom/add`
```json
{
"provider_name": "my-local-oneapi",
"base_url": "http://127.0.0.1:13000/v1",
"model_name": "my-qwen-local",
"display_name": "我的本地 Qwen",
"api_key": "只在请求中出现,服务端转为 secret_ref",
"context_window": 32768,
"supports_tools": true,
"supports_vision": false,
"supports_reasoning": true,
"priority": 10
}
```
### 设置模型优先级
`POST /api/model/priority/set`
```json
{
"items": [
{"model_id": "custom.alice.abc", "priority": 1},
{"model_id": "base.deepseekv4-flash", "priority": 20},
{"model_id": "base.qwen3.6-35b", "priority": 30}
]
}
```
### 解析实际模型路由
`POST /api/model/route/resolve`
系统按用户 priority 排序,选择第一个可用模型;如果没有用户配置,则退回角色默认基础模型。
## 4. Runtime 接口
| 方法 | 路径 | 功能 |
|---|---|---|
| POST | `/api/runtime/select` | 根据 role/tier/risk 选择 shared/dedicated/sandbox |
| POST | `/api/runtime/register` | 注册 runtime 实例 |
| POST | `/api/runtime/heartbeat` | runtime 心跳 |
| GET | `/api/runtime/status` | runtime 列表 |
+23
View File
@@ -0,0 +1,23 @@
# Codex 执行说明:AIOS V1R1C0 全量刷新部署
## 执行目标
把 AIOS v23/v5 WebOps 包升级为 `AIOS V1R1C0 MultiUser GitOps Custom LLM`。
## 严格约束
- 优先复用 NPM,不手写裸 Nginx。
- 优先复用 WebOps/Casdoor,不新增用户体系。
- 优先复用 OpenClaw/Hermes,不新增调度系统。
- Git 服务如果已有则复用;没有才新增 Gitea。
- 不允许把 LLM API Key 写入 Git。
- 不允许普通 Gateway 直接挂载 docker.sock。
## 推荐步骤
```bash
unzip AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip
cd AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706
bash scripts/install_v1r1c0_gitops_model_overlay.sh
bash tests/run_v1r1c0_acceptance.sh
```
+11
View File
@@ -0,0 +1,11 @@
# AIOS V1R1C0 测试与验收方案
自动测试:
```bash
bash tests/run_v1r1c0_acceptance.sh
```
覆盖:API server、添加自定义模型、设置优先级、模型路由、runtime selector、branch、file commit、包完整性。
失败标准:API Key 明文进入 Git、写接口无认证、V 级发布无需 owner 审批、用户可修改他人模型、NPM 被绕过直接暴露服务。
@@ -0,0 +1,7 @@
# AIOS V1R1C0 安全与权限设计
- 用户 API Key 转为 secret file + secret_ref,不进入 Git。
- 普通用户只能修改自己的模型和优先级。
- owner 才能发 V 级版本 tag。
- Docker 操作只能通过 OpenClaw Executor 或受限 Docker Proxy。
- 所有模型、Git、runtime 操作都写审计日志。
+9
View File
@@ -0,0 +1,9 @@
# 用户自定义 LLM 与模型优先级设计
基础模型:`base.deepseekv4-flash`、`base.qwen3.6-35b`、`base.gpt-5.5`。
用户自定义模型支持 OpenAI-compatible provider。API Key 只写入 secret 文件,DB 只保存 `secret://llm/<user>/<id>`。
路由顺序:用户显式 priority → 用户启用自定义模型 → 角色默认基础模型 → 系统兜底模型。
默认 fallback 策略:`try_next_on_error`。
+7
View File
@@ -0,0 +1,7 @@
# NPM 短链接接入说明
统一入口:`https://cnnetmkt.top`。
短路径:`/git /base /ver /diff /patch /model /run /api /hook`。
通过 Nginx Proxy Manager 的 Custom Locations 接入,不覆盖原 NPM 数据。
+7
View File
@@ -0,0 +1,7 @@
# MultiUser Runtime Selector 设计
模式:shared、dedicated、ephemeral_sandbox、fallback_openclaw。
规则:owner/admin/ultra → dedicated;pro → dedicated_or_shared;regular → shared;high_tool/shell/docker/write_prod → ephemeral_sandbox。
用户容器不持有模型 API Key,高危工具进入 sandbox。
+7
View File
@@ -0,0 +1,7 @@
# GitOps 在线基线与 VxRxCx 版本体系
当前版本:`V1R1C0`。
`V1R0C0` 是 ZIP 导入基线;`V1R1C0` 是 MultiUser GitOps + Custom LLM 功能升级。
发布流程:branch → commit → diff → test → tag → rollout → baseline update。
+14
View File
@@ -0,0 +1,14 @@
# AIOS V1R1C0 部署 Runbook
```bash
unzip AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip
cd AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706
bash scripts/install_v1r1c0_gitops_model_overlay.sh
bash tests/run_v1r1c0_acceptance.sh
```
查看 token:
```bash
cat /data/aios-gitops/runtime/secrets/gitops.tokens
```
@@ -0,0 +1,47 @@
# Codex 完整执行指令:AIOS V1R1C0 MultiUser GitOps Custom LLM
你需要基于服务器当前实际情况,完成 AIOS V1R1C0 全量架构刷新与部署。
## 总体目标
1. 在线 GitOps 基线。
2. VxRxCx 版本管理。
3. NPM 短链接入口。
4. MultiUser shared/dedicated runtime selector。
5. 用户自定义 LLM 模型添加。
6. 用户自主设置模型优先级。
7. OpenClaw 命令适配。
8. Hermes 自然语言意图适配。
9. 全量接口测试与验收报告。
## 强制复用
优先复用:NPM、WebOps、Casdoor、OpenClaw、Hermes、One API。
禁止优先新增:新的用户系统、新的入口代理、新的调度平台、普通 Gateway 直接 docker.sock。
## 执行步骤
```bash
unzip AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip
cd AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706
bash scripts/install_v1r1c0_gitops_model_overlay.sh
bash tests/run_v1r1c0_acceptance.sh
```
## NPM 短链接
`/git /api /base /ver /diff /patch /model /run /hook`。
## 成功输出
```text
AIOS_V1R1C0_STATUS=<SUCCESS|FAILED|PARTIAL>
DOMAIN=https://cnnetmkt.top
VERSION=V1R1C0
SHORT_LINKS=/git,/base,/ver,/diff,/patch,/model,/run,/api,/hook
CUSTOM_LLM_API=<OK|FAILED|PARTIAL>
MODEL_PRIORITY=<OK|FAILED|PARTIAL>
RUNTIME_SELECTOR=<OK|FAILED|PARTIAL>
NEXT_UPDATE_MODE=git_patch_only
```
+14
View File
@@ -0,0 +1,14 @@
# ChatGPT / Codex GitOps Access
Entry: https://cnnetmkt.top
Short links: /git, /base, /ver, /diff, /patch, /model, /run, /api, /hook.
All writes must become Git commits. Fixes must use patch branches. Releases must use VxRxCx tags.
Forbidden:
- direct production edits without Git history
- API keys in Git
- API keys in plaintext logs
- bypassing owner/admin approval for V-level changes
+13
View File
@@ -0,0 +1,13 @@
# AIOS VxRxCx Versioning Rules
VxRxCx is the only supported online baseline version format.
- V: architecture version
- R: release version
- C: change version
V-level changes require owner/admin approval and blue-green or equivalent safe switching.
R-level changes require canary or test-environment validation.
C-level changes require smoke test and automatic rollback on failure.
Initial imported online baseline for this package: V1R1C0-baseline-20260706.
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env python3
import sys, pathlib, json
runtime=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else '.').resolve(); (runtime/'.aios_patch_manifests').mkdir(exist_ok=True)
conf=runtime/'webops'/'config'; conf.mkdir(parents=True,exist_ok=True)
(conf/'role_model.active.json').write_text(json.dumps({'roles':['owner','admin','user'],'legacy_role_mapping':{'ultra':'owner','pro':'admin','regular':'user'},'rule':'active runtime authorization must use owner/admin/user only'},ensure_ascii=False,indent=2),encoding='utf-8')
(runtime/'AIOS_ACTIVE_ROLE_MODEL_OWNER_ADMIN_USER').write_text('owner/admin/user\n',encoding='utf-8')
print('ROLE_CONVERGENCE_PATCH_OK', runtime)
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
from pathlib import Path
import sys, json, shutil
root=Path(sys.argv[1]) if len(sys.argv)>1 else Path.cwd()
report={'root':str(root),'created':[],'needs_codex_binding':[]}
patch_dir=root/'runtime_patches'
patch_dir.mkdir(exist_ok=True)
guard=patch_dir/'attachment_reference_route_guard.py'
guard.write_text('''# AIOS v5 route guard
IMAGE_EXT={".jpg",".jpeg",".png",".webp",".gif"}
FILE_EXT={".pdf",".doc",".docx",".xls",".xlsx",".ppt",".pptx",".txt",".csv",".md",".zip",".rar",".7z"}
def classify_message(message_type="text", filename="", mime="", content=""):
t=(message_type or "").lower(); f=(filename or content or "").lower(); m=(mime or "").lower()
if t in ("image","photo","quote_image") or m.startswith("image/") or any(f.endswith(x) for x in IMAGE_EXT): return "vision"
if t in ("file","quote_file") or any(f.endswith(x) for x in FILE_EXT) or (m and not m.startswith("image/")): return "file"
return "text"
def expose_route_proof(role, debug=False):
return bool(debug and role == "owner")
''', encoding='utf-8')
report['created'].append(str(guard))
patterns=['hasHermesReferenceContext','findHermesReferencedMediaItem','cached_image_expired']
for f in list(root.rglob('*.py'))+list(root.rglob('*.js'))+list(root.rglob('*.ts')):
if 'runtime_patches' in str(f) or 'source-baseline' in str(f): continue
try: txt=f.read_text(encoding='utf-8', errors='ignore')
except Exception: continue
if any(p in txt for p in patterns):
backup=f.with_suffix(f.suffix+'.v5guard.bak')
if not backup.exists(): shutil.copy2(f, backup)
if 'AIOS_V5_ROUTE_GUARD_REVIEW_REQUIRED' not in txt:
f.write_text(txt+'\n\n# AIOS_V5_ROUTE_GUARD_REVIEW_REQUIRED: bind runtime_patches.attachment_reference_route_guard.classify_message before Vision routing.\n', encoding='utf-8')
report['needs_codex_binding'].append(str(f))
(root/'runtime_patches'/'v5_patch_report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2), encoding='utf-8')
print(json.dumps(report,ensure_ascii=False,indent=2))
+13
View File
@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS users(id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, display_name TEXT, role TEXT NOT NULL CHECK(role IN ('owner','admin','user')), status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','disabled')), password_hash TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS node_status(node_id TEXT PRIMARY KEY CHECK(node_id IN ('server-hw','server-local')), health TEXT NOT NULL DEFAULT 'unknown', active INTEGER NOT NULL DEFAULT 0, last_seen_at TEXT, score INTEGER NOT NULL DEFAULT 0, note TEXT);
CREATE TABLE IF NOT EXISTS routing_policy(role TEXT PRIMARY KEY CHECK(role IN ('owner','admin','user')), primary_node TEXT NOT NULL CHECK(primary_node IN ('server-hw','server-local')), backup_node TEXT NOT NULL CHECK(backup_node IN ('server-hw','server-local')), active_node TEXT NOT NULL CHECK(active_node IN ('server-hw','server-local')), failover_priority INTEGER NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS approvals(id TEXT PRIMARY KEY, action TEXT NOT NULL, payload TEXT NOT NULL, requested_by TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected','executed','failed')), risk TEXT NOT NULL DEFAULT 'medium', created_at TEXT NOT NULL, decided_by TEXT, decided_at TEXT, result TEXT);
CREATE TABLE IF NOT EXISTS operations(id TEXT PRIMARY KEY, op_type TEXT NOT NULL, payload TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'queued' CHECK(status IN ('queued','running','succeeded','failed','skipped')), created_by TEXT NOT NULL, created_at TEXT NOT NULL, started_at TEXT, finished_at TEXT, result TEXT);
CREATE TABLE IF NOT EXISTS audit_logs(id TEXT PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, target TEXT, payload TEXT, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS events(id TEXT PRIMARY KEY, channel TEXT NOT NULL, actor TEXT, event_type TEXT NOT NULL, payload TEXT NOT NULL, route_target TEXT, status TEXT NOT NULL DEFAULT 'received', created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS quota_policy(username TEXT PRIMARY KEY, daily_limit INTEGER NOT NULL DEFAULT 100, used_today INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS model_policy(role TEXT PRIMARY KEY CHECK(role IN ('owner','admin','user')), default_model TEXT NOT NULL, allowed_models TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS memory_items(id TEXT PRIMARY KEY, username TEXT NOT NULL, scope TEXT NOT NULL DEFAULT 'user', content TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS owner_notifications(id TEXT PRIMARY KEY, event_type TEXT NOT NULL, payload TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'queued', created_at TEXT NOT NULL, sent_at TEXT);
CREATE TABLE IF NOT EXISTS self_optimization_suggestions(id TEXT PRIMARY KEY, category TEXT NOT NULL, severity TEXT NOT NULL, title TEXT NOT NULL, detail TEXT NOT NULL, proposed_fix TEXT, safe_auto_fix INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'open', created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS uat_results(id TEXT PRIMARY KEY, run_id TEXT NOT NULL, tc_id TEXT NOT NULL, category TEXT NOT NULL, status TEXT NOT NULL, latency_ms INTEGER, detail TEXT, created_at TEXT NOT NULL);
+5
View File
@@ -0,0 +1,5 @@
INSERT OR IGNORE INTO node_status(node_id, health, active, score, last_seen_at, note) VALUES ('server-hw','ok',1,100,datetime('now'),'primary logical node'),('server-local','ok',0,95,datetime('now'),'backup logical node');
INSERT OR IGNORE INTO routing_policy(role, primary_node, backup_node, active_node, failover_priority, updated_at) VALUES ('owner','server-hw','server-local','server-hw',1,datetime('now')),('admin','server-hw','server-local','server-hw',2,datetime('now')),('user','server-hw','server-local','server-hw',3,datetime('now'));
INSERT OR IGNORE INTO users(id, username, display_name, role, status, password_hash, created_at, updated_at) VALUES ('bootstrap-owner','owner','Bootstrap Owner','owner','approved','bootstrap-change-me',datetime('now'),datetime('now')),('bootstrap-admin','admin','Bootstrap Admin','admin','approved','bootstrap-change-me',datetime('now'),datetime('now')),('sample-user','sample-user','Sample User','user','approved','bootstrap-change-me',datetime('now'),datetime('now'));
INSERT OR IGNORE INTO model_policy(role, default_model, allowed_models, updated_at) VALUES ('owner','deepseekv4-flash','["deepseekv4-flash","qwen3.6-35b","gpt-5.5"]',datetime('now')),('admin','deepseekv4-flash','["deepseekv4-flash","qwen3.6-35b"]',datetime('now')),('user','deepseekv4-flash','["deepseekv4-flash"]',datetime('now'));
INSERT OR IGNORE INTO quota_policy(username, daily_limit, used_today, updated_at) VALUES ('owner',10000,0,datetime('now')),('admin',2000,0,datetime('now')),('sample-user',200,0,datetime('now'));
@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS gitops_baseline(
id TEXT PRIMARY KEY,
active_version TEXT NOT NULL,
active_tag TEXT NOT NULL,
source_package TEXT,
entry TEXT NOT NULL,
update_mode TEXT NOT NULL DEFAULT 'git_patch_only',
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS gitops_versions(version TEXT PRIMARY KEY, tag TEXT NOT NULL, level TEXT NOT NULL CHECK(level IN ('V','R','C','baseline')), summary TEXT, status TEXT NOT NULL DEFAULT 'draft', created_by TEXT NOT NULL DEFAULT 'system', created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS gitops_patches(id TEXT PRIMARY KEY, patch_name TEXT NOT NULL, branch TEXT NOT NULL, base_ref TEXT NOT NULL DEFAULT 'main', target_version TEXT, risk TEXT NOT NULL DEFAULT 'medium', status TEXT NOT NULL DEFAULT 'draft', created_by TEXT NOT NULL, created_at TEXT NOT NULL, applied_at TEXT, result TEXT);
CREATE TABLE IF NOT EXISTS llm_providers(id TEXT PRIMARY KEY, owner_username TEXT NOT NULL, name TEXT NOT NULL, provider_type TEXT NOT NULL DEFAULT 'openai_compatible', base_url TEXT NOT NULL, auth_mode TEXT NOT NULL DEFAULT 'bearer', api_key_secret_ref TEXT, scope TEXT NOT NULL DEFAULT 'user' CHECK(scope IN ('global','role','user')), status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS llm_models(id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, owner_username TEXT NOT NULL, model_name TEXT NOT NULL, display_name TEXT, context_window INTEGER DEFAULT 0, input_modalities TEXT NOT NULL DEFAULT '["text"]', output_modalities TEXT NOT NULL DEFAULT '["text"]', supports_tools INTEGER NOT NULL DEFAULT 0, supports_vision INTEGER NOT NULL DEFAULT 0, supports_reasoning INTEGER NOT NULL DEFAULT 0, cost_tier TEXT NOT NULL DEFAULT 'custom', is_base INTEGER NOT NULL DEFAULT 0, scope TEXT NOT NULL DEFAULT 'user' CHECK(scope IN ('global','role','user')), status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(owner_username, provider_id, model_name));
CREATE TABLE IF NOT EXISTS user_model_priority(username TEXT NOT NULL, model_id TEXT NOT NULL, priority INTEGER NOT NULL DEFAULT 100, enabled INTEGER NOT NULL DEFAULT 1, fallback_policy TEXT NOT NULL DEFAULT 'try_next_on_error', updated_at TEXT NOT NULL, PRIMARY KEY(username, model_id));
CREATE TABLE IF NOT EXISTS model_route_decisions(id TEXT PRIMARY KEY, username TEXT NOT NULL, task_type TEXT NOT NULL, selected_model_id TEXT, candidate_order TEXT NOT NULL, reason TEXT NOT NULL, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS user_runtime_profiles(username TEXT PRIMARY KEY, role TEXT NOT NULL DEFAULT 'user', tier TEXT NOT NULL DEFAULT 'regular', runtime_mode TEXT NOT NULL DEFAULT 'shared', allow_dedicated INTEGER NOT NULL DEFAULT 0, allow_custom_llm INTEGER NOT NULL DEFAULT 1, updated_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS runtime_instances(id TEXT PRIMARY KEY, username TEXT NOT NULL, runtime_type TEXT NOT NULL, backend TEXT NOT NULL DEFAULT 'hermes', status TEXT NOT NULL DEFAULT 'registered', endpoint TEXT, last_seen_at TEXT, created_at TEXT NOT NULL, metadata TEXT NOT NULL DEFAULT '{}');
CREATE TABLE IF NOT EXISTS model_audit_logs(id TEXT PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, target TEXT, payload TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL);
INSERT OR IGNORE INTO gitops_baseline(id,active_version,active_tag,source_package,entry,update_mode,status,created_at,updated_at) VALUES('current','V1R1C0','V1R1C0','AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706','https://cnnetmkt.top','git_patch_only','active',datetime('now'),datetime('now'));
INSERT OR IGNORE INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES('V1R1C0','V1R1C0','R','Add MultiUser runtime selector, GitOps online baseline and user custom LLM priority registry.','ready','system',datetime('now'));
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT"
bash scripts/package_contract_check_v1r1c0.sh
if [ -f FULL_PACKAGE_FILE_MANIFEST.sha256 ]; then sha256sum -c FULL_PACKAGE_FILE_MANIFEST.sha256 --ignore-missing >/tmp/aios_manifest_check.log || true; fi
echo "AIOS_V1R1C0_PACKAGE_VERIFY_OK"
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env python3
import sys, sqlite3, pathlib, shutil, os
runtime=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else os.environ.get('AIOS_RUNTIME_ROOT','.')).resolve(); webops=runtime/'webops'
(webops/'data').mkdir(parents=True,exist_ok=True); (webops/'migrations').mkdir(parents=True,exist_ok=True)
pkg=pathlib.Path(__file__).resolve().parents[1]
for src in sorted((pkg/'migrations').glob('*.sql')):
dst=webops/'migrations'/src.name
if not dst.exists(): shutil.copy2(src,dst)
con=sqlite3.connect(webops/'data'/'webops.sqlite3')
for sqlf in sorted((webops/'migrations').glob('*.sql')):
con.executescript(sqlf.read_text(encoding='utf-8')); con.commit()
con.close(); print('MIGRATIONS_APPLIED', webops/'data'/'webops.sqlite3')
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
: "${AIOS_RUNTIME_ROOT:?AIOS_RUNTIME_ROOT is required or run from runtime root}"
: "${AIOS_RELEASE_ID:=manual-$(date +%Y%m%dT%H%M%S)}"
BACKUP_DIR="$AIOS_RUNTIME_ROOT/.aios_backups/$AIOS_RELEASE_ID"
mkdir -p "$BACKUP_DIR"
tar --exclude='.aios_backups' --exclude='source-baseline' -czf "$BACKUP_DIR/runtime-before-fix.tgz" -C "$AIOS_RUNTIME_ROOT" .
cat > "$BACKUP_DIR/ROLLBACK.json" <<JSON
{"release_id":"$AIOS_RELEASE_ID","backup":"$BACKUP_DIR/runtime-before-fix.tgz","created_at":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
JSON
echo "BACKUP_CREATED $BACKUP_DIR/runtime-before-fix.tgz"
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
PATCH_NAME="${1:?patch name required}"
BASE="${2:-main}"
cd "${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
git fetch --all || true
git checkout "$BASE"
git pull origin "$BASE" || true
git checkout -B "patch/$PATCH_NAME"
mkdir -p "patches/$PATCH_NAME"
cat > "patches/$PATCH_NAME/patch.json" <<EOF
{"patch":"$PATCH_NAME","branch":"patch/$PATCH_NAME","base":"$BASE","created_at":"$(date -Is)","status":"draft"}
EOF
git add "patches/$PATCH_NAME/patch.json"
git commit -m "patch($PATCH_NAME): create patch branch" || true
git push -u origin "patch/$PATCH_NAME" || true
echo "PATCH_CREATED patch/$PATCH_NAME"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
: "${AIOS_RUNTIME_ROOT:=$(pwd)}"
OUT="${1:-$AIOS_RUNTIME_ROOT/evidence/webops-evidence-$(date +%Y%m%dT%H%M%S).tgz}"
mkdir -p "$(dirname "$OUT")"; tar -czf "$OUT" -C "$AIOS_RUNTIME_ROOT" webops .aios_patch_manifests 2>/dev/null || tar -czf "$OUT" -C "$AIOS_RUNTIME_ROOT" webops; echo "EVIDENCE_EXPORTED $OUT"
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env python3
import pathlib, sys, os, json, hashlib, time
runtime=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else os.environ.get('AIOS_RUNTIME_ROOT','.')).resolve(); out=runtime/'.aios_patch_manifests'; out.mkdir(exist_ok=True)
files=[]
if (runtime/'webops').exists():
for p in (runtime/'webops').rglob('*'):
if p.is_file(): files.append({'path':str(p.relative_to(runtime)),'sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'size':p.stat().st_size})
manifest={'release_id':os.environ.get('AIOS_RELEASE_ID','manual'),'created_at':time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),'files':files,'roles':['owner','admin','user'],'nodes':['server-hw','server-local']}
(out/'release_manifest.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2),encoding='utf-8')
print('RELEASE_MANIFEST_WRITTEN', out/'release_manifest.json')
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
PORT="${AIOS_WEBOPS_PORT:-18081}"
python3 - <<PY
import urllib.request, os
port=os.environ.get('AIOS_WEBOPS_PORT','18081')
print(urllib.request.urlopen(f'http://127.0.0.1:{port}/health/ready',timeout=5).read().decode())
PY
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="${AIOS_GITOPS_ROOT:-/data/aios-gitops}"
WORKSPACE="${AIOS_GITOPS_WORKSPACE:-$ROOT/workspace}"
ZIP_NAME="${1:-AIOS_v23_webops_owner_admin_user_PROD_DEPLOY_20260706.zip}"
mkdir -p "$ROOT/imports" "$WORKSPACE"
ZIP_PATH="$(find /data /opt /root /home -name "$ZIP_NAME" 2>/dev/null | head -1 || true)"
if [ -z "$ZIP_PATH" ]; then echo "BLOCKED: missing $ZIP_NAME"; exit 10; fi
cp "$ZIP_PATH" "$ROOT/imports/"
cd "$WORKSPACE"
unzip -o "$ROOT/imports/$ZIP_NAME"
git init
git config user.name "AIOS GitOps Bot"
git config user.email "gitops-bot@cnnetmkt.top"
git add .
git commit -m "init(baseline): import $ZIP_NAME as V1R0C0" || true
git branch -M main
git tag -a V1R0C0-baseline-20260706 -m "V1R0C0 baseline imported from $ZIP_NAME" || true
echo "BASELINE_IMPORT_OK workspace=$WORKSPACE zip=$ZIP_PATH"
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail
PKG_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
AIOS_GITOPS_ROOT="${AIOS_GITOPS_ROOT:-/data/aios-gitops}"
WORKSPACE="${AIOS_GITOPS_WORKSPACE:-$AIOS_GITOPS_ROOT/workspace}"
LOG_DIR="$AIOS_GITOPS_ROOT/logs"
BACKUP_DIR="$AIOS_GITOPS_ROOT/backups/pre-v1r1c0-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$LOG_DIR" "$BACKUP_DIR" "$WORKSPACE" "$AIOS_GITOPS_ROOT/runtime/secrets"
LOG="$LOG_DIR/install-v1r1c0-$(date +%Y%m%d-%H%M%S).log"
exec > >(tee -a "$LOG") 2>&1
echo "AIOS V1R1C0 GitOps + Custom LLM overlay installer"
echo "PKG_ROOT=$PKG_ROOT"
echo "AIOS_GITOPS_ROOT=$AIOS_GITOPS_ROOT"
echo "WORKSPACE=$WORKSPACE"
echo "== Preflight =="
hostname || true; date -Is; whoami; pwd; df -h || true; free -h || true; docker ps || true; docker compose version || docker-compose version || true; ss -lntp || true
echo "== Backup existing important paths =="
cp -a /data/agent "$BACKUP_DIR/data-agent" 2>/dev/null || true
cp -a /data/aios "$BACKUP_DIR/data-aios" 2>/dev/null || true
cp -a /opt/aios "$BACKUP_DIR/opt-aios" 2>/dev/null || true
cp -a /etc/systemd/system "$BACKUP_DIR/systemd-system" 2>/dev/null || true
echo "== Copy overlay files into GitOps root =="
mkdir -p "$AIOS_GITOPS_ROOT/adapters" "$AIOS_GITOPS_ROOT/scripts" "$AIOS_GITOPS_ROOT/artifacts" "$AIOS_GITOPS_ROOT/runtime"
rsync -a "$PKG_ROOT/adapters/" "$AIOS_GITOPS_ROOT/adapters/"
rsync -a "$PKG_ROOT/scripts/" "$AIOS_GITOPS_ROOT/scripts/"
rsync -a "$PKG_ROOT/config/" "$AIOS_GITOPS_ROOT/config/"
if [ ! -d "$WORKSPACE/.git" ]; then
echo "== Initialize workspace git repository from package content =="
rsync -a --delete --exclude 'source-baseline/*.zip' "$PKG_ROOT/" "$WORKSPACE/"
cd "$WORKSPACE"
git init
git config user.name "AIOS GitOps Bot"
git config user.email "gitops-bot@cnnetmkt.top"
git add .
git commit -m "init(baseline): import AIOS V1R1C0 multiuser gitops custom llm baseline" || true
git branch -M main
git tag -a V1R1C0 -m "AIOS V1R1C0 multiuser gitops custom llm baseline" || true
else
echo "== Existing workspace git repo found; overlay V1R1C0 files =="
rsync -a "$PKG_ROOT/docs/" "$WORKSPACE/docs/"
rsync -a "$PKG_ROOT/config/" "$WORKSPACE/config/"
rsync -a "$PKG_ROOT/migrations/" "$WORKSPACE/migrations/"
rsync -a "$PKG_ROOT/adapters/" "$WORKSPACE/adapters/"
rsync -a "$PKG_ROOT/scripts/" "$WORKSPACE/scripts/"
rsync -a "$PKG_ROOT/tests/" "$WORKSPACE/tests/"
cd "$WORKSPACE"
git add docs config migrations adapters scripts tests VERSION README.md || true
git commit -m "feat(gitops): add V1R1C0 multiuser custom LLM registry and runtime selector" || true
git tag -a V1R1C0 -m "AIOS V1R1C0 feature release" || true
fi
echo "== Token initialization =="
TOKEN_FILE="$AIOS_GITOPS_ROOT/runtime/secrets/gitops.tokens"
if [ ! -s "$TOKEN_FILE" ]; then
(openssl rand -hex 32 || python3 -c 'import secrets; print(secrets.token_hex(32))') > "$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
fi
echo "== Install systemd service for GitOps Model API =="
cat >/etc/systemd/system/aios-gitops-model-api.service <<EOF
[Unit]
Description=AIOS V1R1C0 GitOps Model API
After=network.target
[Service]
Type=simple
Environment=AIOS_GITOPS_ROOT=$AIOS_GITOPS_ROOT
Environment=AIOS_GITOPS_WORKSPACE=$WORKSPACE
Environment=AIOS_GITOPS_DB=$AIOS_GITOPS_ROOT/runtime/gitops_model.sqlite3
Environment=AIOS_GITOPS_TOKEN_FILE=$TOKEN_FILE
Environment=AIOS_GITOPS_HOST=127.0.0.1
Environment=AIOS_GITOPS_PORT=18890
ExecStart=/usr/bin/python3 $AIOS_GITOPS_ROOT/adapters/gitops/aios_gitops_model_api.py
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now aios-gitops-model-api.service
sleep 1
systemctl status aios-gitops-model-api.service --no-pager || true
echo "== NPM reminder =="
cat <<'EOF'
Please configure existing Nginx Proxy Manager custom locations on cnnetmkt.top:
/git -> http://127.0.0.1:3000
/api -> http://127.0.0.1:18890/api
/base -> http://127.0.0.1:18890/base
/ver -> http://127.0.0.1:18890/ver
/diff -> http://127.0.0.1:18890/diff
/patch -> http://127.0.0.1:18890/patch
/run -> http://127.0.0.1:18890/run
/model -> http://127.0.0.1:18890/model
/hook -> existing OpenClaw/WebOps webhook, or 127.0.0.1:18891 if adapter is enabled
EOF
echo "== Local health check =="
curl -fsS http://127.0.0.1:18890/health || true
echo "AIOS_V1R1C0_INSTALL_DONE log=$LOG backup=$BACKUP_DIR"
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
RUNTIME_ROOT="${1:-${AIOS_RUNTIME_ROOT:-$(pwd)}}"
PKG_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
mkdir -p "$RUNTIME_ROOT/webops/app" "$RUNTIME_ROOT/webops/migrations" "$RUNTIME_ROOT/webops/data" "$RUNTIME_ROOT/webops/config" "$RUNTIME_ROOT/webops/scripts"
cp "$PKG_ROOT/active-runtime/app/aios_webops_app.py" "$RUNTIME_ROOT/webops/app/aios_webops_app.py"
cp "$PKG_ROOT"/migrations/*.sql "$RUNTIME_ROOT/webops/migrations/"
cp "$PKG_ROOT/config/domains.env" "$RUNTIME_ROOT/webops/config/domains.env"
cp "$PKG_ROOT/config/roles.yaml" "$RUNTIME_ROOT/webops/config/roles.yaml"
cp "$PKG_ROOT/scripts/run_webops_local.sh" "$RUNTIME_ROOT/webops/scripts/run_webops_local.sh"
chmod +x "$RUNTIME_ROOT/webops/app/aios_webops_app.py" "$RUNTIME_ROOT/webops/scripts/run_webops_local.sh"
echo "WEBOPS_OVERLAY_INSTALLED $RUNTIME_ROOT/webops"
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env python3
import sqlite3, pathlib, os
root=pathlib.Path(os.environ.get('AIOS_WEBOPS_ROOT','.')); db=root/'data'/'webops.sqlite3'
def send_owner_wechat(event_type,payload): print('[OWNER_WECHAT_NOTIFY]',event_type,payload)
with sqlite3.connect(db) as con:
con.row_factory=sqlite3.Row
rs=con.execute("SELECT * FROM owner_notifications WHERE status='queued' ORDER BY created_at LIMIT 20").fetchall()
for r in rs:
send_owner_wechat(r['event_type'],r['payload'])
con.execute("UPDATE owner_notifications SET status='sent',sent_at=datetime('now') WHERE id=?",(r['id'],))
con.commit()
print('OWNER_WECHAT_NOTIFY_WORKER_DONE', len(rs))
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
required=("VERSION" "docs/10_ARCHITECTURE_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_CN.md" "docs/11_USER_REQUIREMENTS_V1R1C0_CN.md" "docs/12_INTERFACE_API_SPEC_V1R1C0_CN.md" "docs/CODEX_FULL_EXECUTION_PROMPT_V1R1C0.md" "active-runtime/app/aios_gitops_model_api.py" "migrations/004_gitops_custom_llm_runtime.sql" "scripts/install_v1r1c0_gitops_model_overlay.sh" "config/model_registry.yaml" "config/runtime_selector.yaml")
for f in "${required[@]}"; do test -f "$ROOT/$f" || { echo "MISSING $f"; exit 1; }; done
grep -q "V1R1C0" "$ROOT/VERSION"
python3 -m py_compile "$ROOT/active-runtime/app/aios_gitops_model_api.py" "$ROOT/adapters/openclaw/openclaw_gitops_commands.py" "$ROOT/adapters/hermes/hermes_gitops_nl_router.py"
echo "PACKAGE_CONTRACT_OK V1R1C0"
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="${1:?version required, e.g. V1R1C0}"
MESSAGE="${2:-release $VERSION}"
if ! echo "$VERSION" | grep -Eq '^V[0-9]+R[0-9]+C[0-9]+$'; then echo "ERROR: version must match VxRxCx"; exit 1; fi
cd "${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
git checkout main
git pull origin main || true
git tag -a "$VERSION" -m "$MESSAGE" || true
git push origin "$VERSION" || true
python3 - <<PY
import json, datetime
from pathlib import Path
p=Path('baseline/current.json')
if p.exists(): d=json.loads(p.read_text())
else: p.parent.mkdir(exist_ok=True); d={}
d['active_version']='$VERSION'; d['active_tag']='$VERSION'; d['updated_at']=datetime.datetime.now(datetime.UTC).isoformat(); d['update_mode']='git_patch_only'
p.write_text(json.dumps(d, ensure_ascii=False, indent=2))
PY
git add baseline/current.json || true
git commit -m "release($VERSION): update active baseline" || true
git push origin main || true
echo "RELEASE_DONE $VERSION"
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
: "${AIOS_RUNTIME_ROOT:=$(pwd)}"
BK="$(find "$AIOS_RUNTIME_ROOT/.aios_backups" -name runtime-before-fix.tgz -type f 2>/dev/null | sort | tail -1)"
[ -n "$BK" ] || { echo "NO_BACKUP_FOUND" >&2; exit 2; }
tmp="$AIOS_RUNTIME_ROOT/.rollback_tmp_$(date +%s)"; mkdir -p "$tmp"; tar -xzf "$BK" -C "$tmp"; rsync -a --delete --exclude='.aios_backups' "$tmp/" "$AIOS_RUNTIME_ROOT/"; rm -rf "$tmp"; echo "ROLLBACK_OK from=$BK"
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
TARGET="${1:?target version required}"
cd "${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
git fetch --all --tags || true
git checkout "$TARGET"
if systemctl list-unit-files | grep -q aios-gitops-model-api.service; then systemctl restart aios-gitops-model-api.service; fi
echo "ROLLBACK_OK target=$TARGET"
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
: "${AIOS_RUNTIME_ROOT:=$(pwd)}"
PORT="${AIOS_WEBOPS_PORT:-18081}"
if ! curl -fsS "http://127.0.0.1:$PORT/health/ready" >/dev/null 2>&1; then
echo "WebOps not running; starting local instance for acceptance"
bash "$AIOS_RUNTIME_ROOT/webops/scripts/run_webops_local.sh" >/tmp/aios-webops-uat.log 2>&1 &
PID=$!
trap 'kill $PID >/dev/null 2>&1 || true' EXIT
sleep 1
fi
python3 "$(dirname "$0")/run_wechat_behavior_simulation.py" || python3 "$(dirname "$0")/self_diagnose.py"
python3 "$(dirname "$0")/run_full_uat.py" || {
echo "UAT failed; running self optimization suggestion pass"
python3 "$(dirname "$0")/self_optimize.py" || true
exit 2
}
echo "ACCEPTANCE_WITH_AUTOFIX_PASS"
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env python3
import urllib.request, urllib.parse, json, http.cookiejar, sys, os
base=os.environ.get('AIOS_WEBOPS_BASE','http://127.0.0.1:18081')
cj=http.cookiejar.CookieJar(); opener=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
def post(path,data):
req=urllib.request.Request(base+path,data=json.dumps(data).encode(),headers={'Content-Type':'application/json'},method='POST')
return json.loads(opener.open(req,timeout=10).read().decode())
def form(path,data):
req=urllib.request.Request(base+path,data=urllib.parse.urlencode(data).encode(),headers={'Content-Type':'application/x-www-form-urlencoded'},method='POST')
return opener.open(req,timeout=10).read().decode()
def get(path): return opener.open(base+path,timeout=10).read().decode()
form('/login',{'username':'owner','password':'bootstrap-change-me'})
ready=get('/health/ready')
assert 'owner' in ready and 'server-hw' in ready
res=post('/api/uat/run',{})
print(json.dumps(res,ensure_ascii=False,indent=2))
if res.get('failed'): sys.exit(2)
print('FULL_UAT_PASS')
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
from __future__ import annotations
import json, os, subprocess, sys, time, urllib.request, tempfile, pathlib
PORT = int(os.getenv('AIOS_TEST_PORT','18990'))
ROOT = pathlib.Path(tempfile.mkdtemp(prefix='aios-v1r1c0-uat-'))
WORKSPACE = ROOT / 'workspace'; WORKSPACE.mkdir()
subprocess.run(['git','init'],cwd=WORKSPACE,check=True,stdout=subprocess.DEVNULL)
subprocess.run(['git','config','user.name','AIOS UAT'],cwd=WORKSPACE,check=True)
subprocess.run(['git','config','user.email','uat@cnnetmkt.top'],cwd=WORKSPACE,check=True)
(WORKSPACE/'README.md').write_text('uat')
subprocess.run(['git','add','.'],cwd=WORKSPACE,check=True)
subprocess.run(['git','commit','-m','init'],cwd=WORKSPACE,check=True,stdout=subprocess.DEVNULL)
subprocess.run(['git','branch','-M','main'],cwd=WORKSPACE,check=True,stdout=subprocess.DEVNULL)
env=os.environ.copy(); env.update({'AIOS_GITOPS_ROOT':str(ROOT),'AIOS_GITOPS_WORKSPACE':str(WORKSPACE),'AIOS_GITOPS_DB':str(ROOT/'runtime'/'gitops_model.sqlite3'),'AIOS_GITOPS_DEV_ALLOW':'1','AIOS_GITOPS_HOST':'127.0.0.1','AIOS_GITOPS_PORT':str(PORT)})
server=pathlib.Path(__file__).resolve().parents[1]/'active-runtime'/'app'/'aios_gitops_model_api.py'
p=subprocess.Popen([sys.executable,str(server)],env=env,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True)
# Wait until the local API is ready; slow CI or busy servers may need a few seconds.
def _wait_ready():
import urllib.error
for _ in range(40):
if p.poll() is not None:
out = p.stdout.read() if p.stdout else ''
raise RuntimeError('server exited early: '+out)
try:
urllib.request.urlopen(f'http://127.0.0.1:{PORT}/health', timeout=1).read()
return
except Exception:
time.sleep(0.25)
out = p.stdout.read() if p.stdout else ''
raise RuntimeError('server not ready: '+out)
_wait_ready()
def call(path,payload=None,user='alice',role='user'):
url=f'http://127.0.0.1:{PORT}{path}'; data=None if payload is None else json.dumps(payload).encode(); req=urllib.request.Request(url,data=data,method='POST' if payload is not None else 'GET')
if data: req.add_header('Content-Type','application/json')
req.add_header('Authorization','Bearer dev'); req.add_header('X-AIOS-User',user); req.add_header('X-AIOS-Role',role)
with urllib.request.urlopen(req,timeout=10) as r: return json.loads(r.read().decode())
ok=[]
try:
ok.append(('health',call('/health')['status']=='ok'))
add=call('/api/model/custom/add',{'provider_name':'local-test','base_url':'http://127.0.0.1:9999/v1','model_name':'test-model','priority':5},role='user')
ok.append(('custom_model_add',add.get('status')=='ok'))
pr=call('/api/model/priority/set',{'items':[{'model_id':add['model_id'],'priority':1},'base.deepseekv4-flash']},role='user')
ok.append(('priority_set',pr.get('status')=='updated'))
route=call('/api/model/route/resolve',{'task_type':'chat'},role='user')
ok.append(('route_resolve',route.get('selected',{}).get('id')==add['model_id']))
rt=call('/api/runtime/select',{'tier':'regular','risk':'normal'},role='user')
ok.append(('runtime_select_regular',rt.get('runtime_type')=='shared'))
b=call('/api/branch',{'branch':'patch/uat'},role='admin')
ok.append(('branch_create',b.get('status')=='ok'))
f=call('/api/file',{'branch':'patch/uat','path':'docs/uat.md','content':'ok','message':'test: uat file'},role='admin')
ok.append(('file_commit',f.get('status')=='committed'))
finally:
p.terminate()
try: p.communicate(timeout=5)
except Exception: pass
failed=[name for name,passed in ok if not passed]
print(json.dumps({'status':'PASS' if not failed else 'FAIL','cases':ok,'failed':failed,'root':str(ROOT)},ensure_ascii=False,indent=2))
raise SystemExit(0 if not failed else 1)
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
from pathlib import Path
import os, sys, time, subprocess, json, urllib.request, urllib.parse, http.cookiejar, socket
root=Path(__file__).resolve().parents[1]
app=root/'active-runtime'/'app'/'aios_webops_app.py'
port=int(os.environ.get('AIOS_WEBOPS_UAT_PORT','18189'))
env=os.environ.copy()
env.setdefault('AIOS_WEBOPS_ROOT', str(root/'active-runtime'))
env.setdefault('AIOS_WEBOPS_SECRET','uat-secret')
env.setdefault('AIOS_OWNER_USERNAME','owner')
env.setdefault('AIOS_OWNER_PASSWORD','bootstrap-change-me')
env['AIOS_WEBOPS_PORT']=str(port)
env['AIOS_WEBOPS_DB']=str(root/'active-runtime'/'data'/'uat_webops_v5.sqlite3')
try:
(root/'active-runtime'/'data'/'uat_webops_v5.sqlite3').unlink()
except FileNotFoundError:
pass
p=subprocess.Popen([sys.executable,str(app)], env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
base=f'http://127.0.0.1:{port}'
try:
# wait for ready
last=''
for _ in range(30):
try:
with urllib.request.urlopen(base+'/health/ready', timeout=2) as r:
ready=json.loads(r.read().decode())
break
except Exception as e:
last=str(e); time.sleep(0.3)
else:
raise RuntimeError('webops not ready: '+last)
assert ready['roles']==['owner','admin','user'], ready
assert 'server-hw' in ready['nodes'] and 'server-local' in ready['nodes'], ready
# login owner and verify core pages
cj=http.cookiejar.CookieJar()
opener=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
data=urllib.parse.urlencode({'username':'owner','password':'bootstrap-change-me'}).encode()
opener.open(urllib.request.Request(base+'/login', data=data, method='POST'), timeout=5).read()
for page in ['/dashboard','/nodes','/roles','/routing','/failover','/approvals','/operations','/users','/quota','/models','/memory','/channels/wechat','/life','/patches','/optimization','/simulation','/logs','/config','/uat','/versions','/evidence']:
html=opener.open(base+page, timeout=5).read().decode(errors='ignore')
assert '<html' in html.lower() or '<!doctype' in html.lower(), page
# run UAT through API
req=urllib.request.Request(base+'/api/uat/run', data=b'{}', headers={'Content-Type':'application/json'}, method='POST')
res=json.loads(opener.open(req, timeout=10).read().decode())
assert res.get('status')=='PASS', res
print('V5_UAT_OK', json.dumps({'ready':ready,'uat':res}, ensure_ascii=False))
finally:
p.terminate()
try: p.wait(timeout=5)
except Exception: p.kill()
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
WEBOPS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export AIOS_WEBOPS_ROOT="$WEBOPS_ROOT"
if [ -f "$WEBOPS_ROOT/config/domains.env" ]; then set -a; . "$WEBOPS_ROOT/config/domains.env"; set +a; fi
: "${AIOS_WEBOPS_PORT:=18081}"
python3 "$WEBOPS_ROOT/app/aios_webops_app.py"
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env python3
import urllib.request, urllib.parse, json, http.cookiejar, os, sys
base=os.environ.get('AIOS_WEBOPS_BASE','http://127.0.0.1:18081')
cj=http.cookiejar.CookieJar(); opener=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
def form(path,data):
req=urllib.request.Request(base+path,data=urllib.parse.urlencode(data).encode(),headers={'Content-Type':'application/x-www-form-urlencoded'},method='POST')
return opener.open(req,timeout=10).read().decode()
def post(path,data):
req=urllib.request.Request(base+path,data=json.dumps(data).encode(),headers={'Content-Type':'application/json'},method='POST')
return json.loads(opener.open(req,timeout=10).read().decode())
form('/login',{'username':'admin','password':'bootstrap-change-me'})
cases=[('text','你好','text'),('quote_text','看一下这个方案','text'),('image','识别图片','vision'),('quote_image','这张图是什么','vision'),('file_pdf','总结一下文件','file'),('file_excel','看一下表格','file'),('quote_file','这个文件有什么问题','file')]
bad=[]
for typ,msg,expect in cases:
r=post('/api/simulation/wechat',{'username':'sample-user','message_type':typ,'content':msg})
print(typ, r)
if r.get('route_target')!=expect: bad.append((typ,expect,r))
if bad:
print('WECHAT_SIM_FAIL',bad); sys.exit(2)
print('WECHAT_BEHAVIOR_SIMULATION_PASS')
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env python3
import urllib.request, urllib.parse, json, http.cookiejar, os
base=os.environ.get('AIOS_WEBOPS_BASE','http://127.0.0.1:18081')
cj=http.cookiejar.CookieJar(); opener=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
def form(path,data):
req=urllib.request.Request(base+path,data=urllib.parse.urlencode(data).encode(),headers={'Content-Type':'application/x-www-form-urlencoded'},method='POST')
return opener.open(req,timeout=10).read().decode()
def post(path,data):
req=urllib.request.Request(base+path,data=json.dumps(data).encode(),headers={'Content-Type':'application/json'},method='POST')
return json.loads(opener.open(req,timeout=10).read().decode())
form('/login',{'username':'admin','password':'bootstrap-change-me'})
print(json.dumps(post('/api/optimization/diagnose',{}),ensure_ascii=False,indent=2))
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
from pathlib import Path
import os, sqlite3, json, time, uuid
root=Path(os.getenv('AIOS_WEBOPS_ROOT', Path(__file__).resolve().parents[1]/'active-runtime'))
db_path=Path(os.getenv('AIOS_WEBOPS_DB', root/'data'/'webops.sqlite3'))
if not db_path.exists():
db_path=root/'data'/'webops_v5.sqlite3'
db_path.parent.mkdir(parents=True, exist_ok=True)
con=sqlite3.connect(db_path); con.row_factory=sqlite3.Row
now=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
def uid(p): return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
findings=[]
try:
roles=[r['role'] for r in con.execute('SELECT role FROM routing_policy ORDER BY failover_priority')]
if roles!=['owner','admin','user']:
findings.append(('routing','high','角色迁移顺序异常',str(roles),'修复为 owner/admin/user'))
bad=list(con.execute("SELECT username,role FROM users WHERE role NOT IN ('owner','admin','user')"))
if bad:
findings.append(('security','critical','存在非法角色',json.dumps([dict(x) for x in bad],ensure_ascii=False),'迁移非法角色为 user'))
except Exception as e:
findings.append(('schema','medium','诊断失败',str(e),'运行迁移后复测'))
for cat,sev,title,detail,fix in findings or [('health','low','未发现阻塞问题','静态策略检查通过','无须处理')]:
con.execute('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(uid('suggest'),cat,sev,title,detail,fix,1 if sev in ('low','medium') else 0,'open',now))
con.commit(); con.close()
print(json.dumps({'diagnosis':'ok','findings':len(findings)},ensure_ascii=False))
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env python3
import subprocess, sys, os
print('SELF_OPTIMIZE_START: generating diagnosis suggestions')
subprocess.run([sys.executable, os.path.join(os.path.dirname(__file__),'self_diagnose.py')], check=False)
print('SELF_OPTIMIZE_DONE: suggestions generated in WebOps /optimization. Owner approval required for risky fixes.')
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env python3
from pathlib import Path
import os, sqlite3, time, argparse
parser=argparse.ArgumentParser(); parser.add_argument('--safe-only', action='store_true'); args=parser.parse_args()
root=Path(os.getenv('AIOS_WEBOPS_ROOT', Path(__file__).resolve().parents[1]/'active-runtime'))
db_path=Path(os.getenv('AIOS_WEBOPS_DB', root/'data'/'webops.sqlite3'))
if not db_path.exists():
db_path=root/'data'/'webops_v5.sqlite3'
db_path.parent.mkdir(parents=True, exist_ok=True)
con=sqlite3.connect(db_path)
now=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
# Safe-only: only restore role migration order and illegal roles. Runtime/code changes still require owner approval.
con.execute("UPDATE routing_policy SET failover_priority=1,active_node='server-hw',backup_node='server-local',updated_at=? WHERE role='owner'",(now,))
con.execute("UPDATE routing_policy SET failover_priority=2,active_node='server-hw',backup_node='server-local',updated_at=? WHERE role='admin'",(now,))
con.execute("UPDATE routing_policy SET failover_priority=3,active_node='server-hw',backup_node='server-local',updated_at=? WHERE role='user'",(now,))
con.execute("UPDATE users SET role='user',updated_at=? WHERE role NOT IN ('owner','admin','user')",(now,))
con.commit(); con.close()
print('SELF_OPTIMIZE_V5_SAFE_FIX_OK')
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
bash "$ROOT/scripts/silent_install.sh"
echo "V5_SILENT_INSTALL_OK"
echo "WebOps: https://ops.cnnetmkt.top"
echo "Digital Life: https://ops.cnnetmkt.top/life"
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
RUNTIME_ROOT="${1:-${AIOS_RUNTIME_ROOT:-$(pwd)}}"
python3 -m py_compile "$RUNTIME_ROOT/webops/app/aios_webops_app.py"
python3 "$RUNTIME_ROOT/webops/app/aios_webops_app.py" >/tmp/aios-webops-smoke.log 2>&1 &
PID=$!
trap 'kill $PID >/dev/null 2>&1 || true' EXIT
sleep 1
PORT="${AIOS_WEBOPS_PORT:-18081}"
python3 - <<PY
import urllib.request, os
port=os.environ.get('AIOS_WEBOPS_PORT','18081')
resp=urllib.request.urlopen(f'http://127.0.0.1:{port}/health/ready',timeout=5).read().decode()
assert 'owner' in resp and 'server-hw' in resp and 'cnnetmkt.top' in resp
print('WEBOPS_SMOKE_OK')
PY
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env python3
import sys, pathlib, re, os
runtime=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else os.environ.get('AIOS_RUNTIME_ROOT','.')).resolve(); legacy=re.compile(r'\b(ultra|pro|regular)\b',re.I); viol=[]
for base in [runtime/'webops']:
if base.exists():
for p in base.rglob('*'):
if p.is_file() and p.suffix.lower() in ['.py','.sh','.yaml','.yml','.json','.env','.md','.html','.js','.sql']:
s=p.read_text(errors='ignore')
if 'legacy_role_mapping' in s: continue
if legacy.search(s): viol.append(str(p))
if viol:
print('LEGACY_ROLE_REFERENCES_FOUND'); print('\n'.join(viol[:20])); raise SystemExit(2)
print('NO_LEGACY_ROLES_IN_ACTIVE_OVERLAY_OK')
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
import sys, pathlib, os
runtime=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else os.environ.get('AIOS_RUNTIME_ROOT','.')).resolve()
candidates=[
runtime/'webops/config/domains.env',
runtime/'webops/config/roles.yaml',
runtime/'AIOS_ACTIVE_ROLE_MODEL_OWNER_ADMIN_USER',
runtime/'config/domains.env',
runtime/'config/roles.yaml',
runtime/'config/nodes.yaml',
runtime/'config/routing.yaml',
]
text=''
for p in candidates:
if p.exists():
text += p.read_text(errors='ignore')+'\n'
required=['owner','admin','user','server-hw','server-local','https://cnnetmkt.top','https://ops.cnnetmkt.top']
missing=[x for x in required if x not in text]
if missing:
raise SystemExit('VALIDATE_ROLES_DOMAINS_FAIL '+str(missing))
print('VALIDATE_ROLES_DOMAINS_OK')
@@ -0,0 +1,20 @@
{
"source_zip": "aios_v23_RUNTIME_FULL_DEPLOY_20260706T113802_ATTACHMENT_ROUTE_GUARD_FULL.zip",
"source_file_count": 1168,
"active_role_model": [
"owner",
"admin",
"user"
],
"logical_nodes": [
"server-hw",
"server-local"
],
"domains": {
"public_user_domain": "https://cnnetmkt.top",
"webops_domain": "https://ops.cnnetmkt.top",
"api_domain": "https://api.cnnetmkt.top",
"wechat_callback_domain": "https://wechat.cnnetmkt.top"
},
"purpose": "delta-only WebOps full refresh on top of latest actual deployment package"
}
@@ -0,0 +1,22 @@
{
"current_actual": {
"name": "aios_v23_RUNTIME_FULL_DEPLOY_20260706T113802_ATTACHMENT_ROUTE_GUARD_FULL.zip",
"size": 5237501,
"sha256": "8456cf86202ef46f9ca6e61b14f1763f298755e5eb36d42051306620c49f471c",
"files": 1168,
"docs": 513,
"scripts": 324,
"configs": 112,
"tests": 189
},
"base_delta": {
"name": "AIOS_v23_webops_full_refresh_PROD_DELTA_20260706.zip",
"size": 4898201,
"sha256": "0ebedbf51b658f4d98fa09e1e6169c48e4396ac33ebab0088a351de313501b73",
"files": 44,
"docs": 11,
"scripts": 22,
"configs": 9,
"tests": 2
}
}
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
PKG_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
: "${AIOS_RUNTIME_ROOT:=$PKG_ROOT/.test_runtime}"
rm -rf "$AIOS_RUNTIME_ROOT"; mkdir -p "$AIOS_RUNTIME_ROOT"
bash "$PKG_ROOT/scripts/install_webops_overlay.sh" "$AIOS_RUNTIME_ROOT"
python3 "$PKG_ROOT/scripts/apply_migrations.py" "$AIOS_RUNTIME_ROOT"
for s in run_full_uat.py run_wechat_behavior_simulation.py self_diagnose.py self_optimize.py run_acceptance_with_autofix.sh; do cp "$PKG_ROOT/scripts/$s" "$AIOS_RUNTIME_ROOT/webops/scripts/$s"; chmod +x "$AIOS_RUNTIME_ROOT/webops/scripts/$s"; done
bash "$AIOS_RUNTIME_ROOT/webops/scripts/run_webops_local.sh" >/tmp/aios-webops-pkg-uat.log 2>&1 &
PID=$!
trap 'kill $PID >/dev/null 2>&1 || true' EXIT
sleep 1
AIOS_WEBOPS_BASE="http://127.0.0.1:${AIOS_WEBOPS_PORT:-18081}" bash "$AIOS_RUNTIME_ROOT/webops/scripts/run_acceptance_with_autofix.sh"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
python3 scripts/run_v1r1c0_uat.py
bash scripts/package_contract_check_v1r1c0.sh
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
python3 "$ROOT/scripts/validate_roles_and_domains.py"
python3 "$ROOT/scripts/validate_no_legacy_roles.py"
if ! python3 "$ROOT/scripts/run_v5_uat.py"; then
echo "V5 UAT failed; running self-diagnosis and safe rollback-ready repair plan" >&2
AIOS_WEBOPS_ROOT="$ROOT/active-runtime" python3 "$ROOT/active-runtime/scripts/apply_migrations.py" || true
AIOS_WEBOPS_ROOT="$ROOT/active-runtime" python3 "$ROOT/scripts/self_diagnose_v5.py" || true
AIOS_WEBOPS_ROOT="$ROOT/active-runtime" python3 "$ROOT/scripts/self_optimize_v5.py" --safe-only || true
python3 "$ROOT/scripts/run_v5_uat.py"
fi
echo "V5_ACCEPTANCE_WITH_AUTOFIX_OK"
+4
View File
@@ -0,0 +1,4 @@
from pathlib import Path
def test_api_file_exists():
assert (Path(__file__).resolve().parents[1] / 'active-runtime/app/aios_gitops_model_api.py').exists()

Some files were not shown because too many files have changed in this diff Show More