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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions LOCAL_PREVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ node local-preview.cjs

当前助手为本地概念与交互演示:使用预设资料导航和示例回复,没有连接真实模型,也不会发送模型 API 请求。

项目区新增“一键 Star 全部仓库”。实际使用需配置项目自有的 GitHub App 和授权服务,见 [STAR_SETUP.md](STAR_SETUP.md)。未配置时会显示仓库清单和明确提示,不会假报 Star 成功。

无需 GitHub App 也能体验完整操作:运行 `node local-preview.cjs --star-demo`,点击“一键 Star 全部仓库”→“体验模拟授权”→“模拟允许并返回主页”。返回后自动展示 11 个仓库的模拟结果。页面始终有模拟标识,不会请求 GitHub 或修改真实 Star。正式授权采用 GitHub 登录后自动返回,无需用户输入验证码或复制令牌。

建议测试:导航栏右上角的小矩头像;导航栏滚出视野后出现的右下角透明全身贴图(桌面高 120px,手机高 100px);回到顶部时贴图收起;面板展开与收起、快捷问题、输入发送、项目上下文、手机窄屏布局及键盘操作。首页正文没有角色立绘或欢迎卡片。资料链接可跳转至外部项目页面。

若在终端前台启动,测试结束后可在该终端按 `Ctrl+C` 结束本次服务。
Expand Down
14 changes: 10 additions & 4 deletions SHA256SUMS
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ cd03e6eab67eed80970fc2e7fb8a0d6e3f65c9bda7e8ea27e02a87adde06ecd2 .gitignore
dc2b68dea37313b64e7a5e585f68be9164e3ebfae84ed5614a28519159a68454 ATTRIBUTION.md
a7b206789701abc1335853910298dcd9b397e9521704759c225dcd0d5fd2f61c BENCHMARK_EVIDENCE.json
304b20c046a82dd9cd6269151ecbe09f7807fd81376c227121f77517321cb126 INFERRSI.md
9b37f8f61e35ca9998499716bd299a03f05d81208acae3bb35900807a0fc55ad LOCAL_PREVIEW.md
49ba892018cdb9707293ffa38351b63834234ad258430efbbd792b42043c124e LOCAL_PREVIEW.md
51ce2153087f31b813b2ba275c3d9456af4e5a951f46ebaf97d59ee9ce10d8aa README.md
fafa3b155e999c060cdd9b884484c968348929397362a03483dabfd84ef2a62e SHOWCASE.md
5b52331f0c9bfa13ec0174f62be1efd764fe03ed19ca659c5868bf42245dc387 STAR_SETUP.md
0bc3d5f7bd34ac53892f39ab97de48a9d771ddbfc35676255fa46292e4dcae81 app.js
53cf5937ee480102fbabd8e26a52adf1e936a49c2c24d0fc1948465982466017 assets/architecture/afd.svg
c2d920840043570ad8b25f1f43f87a2afe72c7e7bbac5a7309b599929032bd45 assets/architecture/afd.svg.license
Expand Down Expand Up @@ -39,17 +40,22 @@ ca3598fb776abba7c70b96199440da23ef9ba6af9456f594c810e00b9b1761ec assets/showcas
8e7e0bdd2bbe622dd85b5c06ba508293ab9bb3ae8f4f530c4104ed89af5bd536 assets/showcase/workswarm.jpg
f1e5d4f2cc41c9c1da69dfb3523c5659012ac8169d06c267b165af5ecd2aa698 assets/showcase/workswarm.mp4
30e9b46a6d06ab864bbc7ca0289143822d1192392074dc965b0683559f6911e0 assistant.css
71ba38902b5a33b28d7bb66f5a90d83a73cbc4ca9b0d037289b7abce221a30c2 assistant.js
28b135f8c6626c4ae8bd98140a2c32794a1bf718a899ea6030f47f4d7830a1d7 assistant.js
1c13002720c8d65a3fc98084c708b9058827ab66740ae52105991cfebc60eb8e dom-fixture.json
e8a9bbc48eca6225629cc2cbb31629886dfd72228fac74875856bd31b597e245 index.html
55f84f9a0a4f1f63709cd9fb43693d5552f86b658305410375061444ba456b7d index.html
acfa2bbd15ebbe680b46d0a542b4af11b893f425340d8ffe13e35f1f66523706 inferrsi.js
ae0fe01b8c81c20e4a607f8608b84ac0df3b42f9e38bcaaefde92cda5c63a782 initial-data.json
0e8566ea362276878e1f38f97da49964dbea301c15d4aa9d727f97a4c39fd943 licenses/afd-plugin-LICENSE.txt
c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 licenses/vllm-omni-LICENSE.txt
19cc4e8b177647501dc4c8fa02ce38e90f0c1f3efe70fe2aa3409930bde7aae1 local-preview.cjs
9cf05d3e1319bd85148c179fb934925883df8dff4f836dbfc53d2626ccfcedb4 local-preview.cjs
250f9ee919523549bbabe67fa4585a2772a405f8364b2a370b4020e61aedb8b6 prepare-initial-content.cjs
33b901a79c77f170fd4f11d7a67113f31f96bf91c58006d8d3d5a7530bb90d58 qa-interactions.cjs
536fe19b242a6c8e5bebf4229c7081b4ab82bb08627b7c6c3c1d32495ffd590c qa-stars.cjs
51a3881b90c67476605580f3a859becf174b8bf67d37c5c1200324613f2ab42d robots.txt
62f5e8d2444784d0e8df287e3b28931d1a259bf61d71c0636abb8a16add3ba31 showcase.js
741e991c7d0bc12c49acc061ba697dcc5e57946380c708cc823f7fadcb3d6485 sitemap.xml
a6c7baab91939560795b458fe0246e7a111f128badc135e1b766f5a46f806ff2 star-repositories.json
20c327926e78e187d970c6e83a6ea1e4131cbea0fdf703781aa3f88ce37e1116 star-service.cjs
90f0dbbeb5f1e180c83769982124c455fe21fe1489a01aaa0e93845a5932b51d stars.css
8b53a00d84c1e1096e7f0fc91da3f2af203e4709fa0bae9d5ee062174eb97125 stars.js
8ad287859fb0665c8114c44cce235ec47974c9dae9204bf9775540f3c430c712 styles.css
67 changes: 67 additions & 0 deletions STAR_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Bulk Star service

The homepage lists all 11 GitHub repositories linked in its HTML, including the
seven ecosystem projects, three engine examples and the marketing asset library.
`star-repositories.json` is the server allowlist; update it alongside homepage
links. `node --test qa-stars.cjs` checks coverage and the authorization boundary.

## Local test without a GitHub App

Run `node local-preview.cjs --star-demo`, then open
<http://127.0.0.1:4173/#projects>. Click the bulk-Star entry, choose simulated
authorization, and approve on the clearly labelled local confirmation page.
It returns to the homepage and shows simulated per-repository results. There
are no verification codes, credentials or GitHub requests in this mode. The
banner and results explicitly say simulation. Demo mode refuses public origins
and is disabled by default. Do not expose the demo server publicly.

## Configure a project-owned GitHub App (once, by the operator)

1. Register a public GitHub App with the project's homepage URL. Add the exact
callback URL `<PUBLIC_ORIGIN>/api/stars/callback`. For local real integration,
register `http://127.0.0.1:4173/api/stars/callback` as well. Disable webhooks if
unused. Device Flow is not needed.
2. Grant **Account permissions → Starring: read and write** and the required
**Repository permissions → Metadata: read**. Do not grant code-write access.
3. Inject the Client ID as `GITHUB_STAR_CLIENT_ID` and the Client Secret as
`GITHUB_STAR_CLIENT_SECRET` into the server process at runtime. Keep the
secret in the hosting provider's secret store; never send it in chat or
commit it to this repository. User tokens remain server-side as well.
4. Run `node local-preview.cjs` (Node 22+). Open <http://127.0.0.1:4173/>.
Visitors click “Authorize on GitHub & star”, sign in if needed, and approve.
GitHub returns them to the homepage, which automatically sends the batch and
shows individual results. No code entry, token copy or extra callback click
is needed. The repository list is expandable before authorization. Retry
skips successful items; existing stars are preserved by GitHub's idempotent PUT.

## Hosting

GitHub Pages serves static files and cannot run the authorization API. Keep the
static site on Pages and route `/api/stars/*` through a same-origin HTTPS reverse
proxy to a Node service invoking `createStarService` from `star-service.cjs`.
Set `STAR_PUBLIC_ORIGIN` to the exact public HTTPS origin. The included preview
server binds loopback only; a production process can remain behind the local
reverse proxy. Forward API responses unchanged, including `Set-Cookie`, and do
not cache them. Do not enable cross-origin API access or log cookies, tokens,
upstream payloads, or authorization codes. Apply edge request rate limits before
public exposure, especially to `/start`. Host the single-process API separately
from static file delivery; in-memory sessions are not shared between replicas.

Tokens stay only in server memory, bound to a random HttpOnly/SameSite=Lax cookie.
The web callback checks a session-bound random state and exchanges the code
with S256 PKCE. The callback can be consumed only once; the frontend consumes
an additional one-time resume marker before starting the automatic batch.
Sessions expire within 15 minutes; browser close of the dialog discards the
session, stopping remaining requests after any in-flight request. Successful
stars remain. Server restart requires fresh authorization. No refresh token is
retained. Users can revoke app authorization in GitHub settings. Errors and
limits are reported without exposing GitHub payloads. No real stars are made by
the test suite: it injects a simulated GitHub transport. Production authorization
has not been verified until a project-owned app and hosting are configured.

If the service is unavailable or unconfigured, the page shows a clear notice and
individual repository links. It never substitutes opening tabs for a successful
batch operation.

References: [GitHub App web authorization](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-web-application-flow-to-generate-a-user-access-token),
[Starring API and permissions](https://docs.github.com/en/rest/activity/starring#star-a-repository-for-the-authenticated-user).
2 changes: 1 addition & 1 deletion assistant.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
<link rel="icon" href="assets/favicon.svg?v=exact-logo" type="image/svg+xml">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="assistant.css">
<link rel="stylesheet" href="stars.css">
<script src="app.js" defer></script>
<script src="showcase.js" defer></script>
<script src="inferrsi.js" defer></script>
<script src="assistant.js" defer></script>
<script src="stars.js" defer></script>
</head>
<body>
<a class="skip-link" href="#main">跳转到主要内容</a>
Expand Down
8 changes: 6 additions & 2 deletions local-preview.cjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
// Local-only static preview. No dependencies, AI requests or credentials.
// Loopback preview with optional GitHub Star service; no model requests.
const http = require('node:http');
const fs = require('node:fs');
const path = require('node:path');
const root = __dirname;
const {createStarService}=require('./star-service.cjs');
const starDemo=process.argv.includes('--star-demo') || process.env.STAR_LOCAL_DEMO==='1';
const starService=createStarService({clientId:process.env.GITHUB_STAR_CLIENT_ID,clientSecret:process.env.GITHUB_STAR_CLIENT_SECRET,demo:starDemo,origin:process.env.STAR_PUBLIC_ORIGIN || `http://127.0.0.1:${process.env.MIRA_PREVIEW_PORT || 4173}`});
const types = {'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'text/javascript; charset=utf-8','.json':'application/json; charset=utf-8','.svg':'image/svg+xml','.png':'image/png','.webp':'image/webp','.jpg':'image/jpeg','.jpeg':'image/jpeg','.mp4':'video/mp4','.txt':'text/plain; charset=utf-8'};
const server = http.createServer((req,res)=>{
if(req.url.startsWith('/api/stars/')){starService(req,res).catch(()=>{if(!res.headersSent)res.writeHead(500);res.end();});return;}
if(!['GET','HEAD'].includes(req.method)){res.writeHead(405);res.end();return;}
let pathname;try{pathname=decodeURIComponent(new URL(req.url,'http://localhost').pathname);}catch{res.writeHead(400);res.end();return;}
if(pathname.includes('\0')){res.writeHead(400);res.end();return;}
Expand All @@ -17,4 +21,4 @@ const server = http.createServer((req,res)=>{
});
});
server.on('error',error=>{console.error(error.message);process.exit(1);});
server.listen(Number(process.env.MIRA_PREVIEW_PORT || 4173),'127.0.0.1',()=>console.log(`MIRA local preview: http://127.0.0.1:${server.address().port}`));
server.listen(Number(process.env.MIRA_PREVIEW_PORT || 4173),'127.0.0.1',()=>console.log(`MIRA local preview: http://127.0.0.1:${server.address().port}${starDemo?' (local Star simulation, no GitHub requests)':''}`));
28 changes: 28 additions & 0 deletions qa-stars.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';
const {test}=require('node:test'),assert=require('node:assert/strict'),http=require('node:http'),fs=require('node:fs'),{createHash}=require('node:crypto');
const {createStarService}=require('./star-service.cjs');
const repos=require('./star-repositories.json'),origin='https://example.com';
async function fixture(t,{configured=true,status=204,exchangeError=false,demo=false}={}){
let time=100000,puts=[],exchanges=[],challenge;
const fetcher=async(url,options)=>{
if(demo)throw Error('Demo must never call GitHub');
if(url.endsWith('/access_token')){const params=new URLSearchParams(options.body);exchanges.push(params);assert.equal(params.get('client_secret'),'synthetic-secret');assert.equal(params.get('redirect_uri'),origin+'/api/stars/callback');assert.equal(createHash('sha256').update(params.get('code_verifier')).digest('base64url'),challenge);return Response.json(exchangeError?{error:'bad_code'}:{access_token:'synthetic-token'});}
assert.match(url,/^https:\/\/api.github.com\/user\/starred\//);assert.equal(options.method,'PUT');assert.equal(options.headers.Authorization,'Bearer synthetic-token');puts.push(url.split('/starred/')[1]);return new Response(null,{status:typeof status==='function'?status(puts.length):status});
};
const serviceOrigin=demo?'http://127.0.0.1:4173':origin;
const handler=createStarService({clientId:configured?'synthetic-client':'',clientSecret:configured?'synthetic-secret':'',origin:serviceOrigin,fetcher,now:()=>time,pause:async()=>{},demo});
const server=http.createServer((req,res)=>handler(req,res));await new Promise(r=>server.listen(0,'127.0.0.1',r));t.after(()=>new Promise(r=>server.close(r)));
let cookie='';
const request=async(path,options={})=>{const r=await fetch(`http://127.0.0.1:${server.address().port}/api/stars/${path}`,{method:'POST',redirect:'manual',...options,headers:{Origin:serviceOrigin,'X-Requested-With':'InferMatrixStars',Cookie:cookie,...options.headers}});const c=r.headers.get('set-cookie');if(c)cookie=c.split(';')[0];const raw=await r.text();return {status:r.status,data:r.headers.get('content-type')?.includes('application/json')?JSON.parse(raw):null,text:raw,cookie:c,location:r.headers.get('location')};};
const start=async()=>{const r=await request('start');const u=new URL(r.data.authorizeUrl);challenge=u.searchParams.get('code_challenge');return {response:r,url:u,state:u.searchParams.get('state')};};
return {request,start,advance:ms=>time+=ms,puts,exchanges,login:async()=>{const d=await start();return request('callback?state='+d.state+'&code=synthetic-code',{method:'GET'});}};
}
test('catalog matches all homepage repositories without duplicates',()=>{const linked=[...fs.readFileSync('index.html','utf8').matchAll(/https:\/\/github.com\/([\w.-]+\/[\w.-]+)/g)].map(m=>m[1].toLowerCase());assert.deepEqual([...new Set(linked)].sort(),repos.map(r=>r.toLowerCase()).sort());assert.equal(new Set(repos.map(r=>r.toLowerCase())).size,repos.length);});
test('unconfigured service explicitly disables authorization',async t=>{const f=await fixture(t,{configured:false});assert.equal((await f.request('status',{method:'GET'})).data.configured,false);assert.equal((await f.request('start')).status,503);});
test('cross-origin or missing header cannot start or star',async t=>{const f=await fixture(t);assert.equal((await f.request('start',{headers:{Origin:'https://other.example'}})).status,403);assert.equal((await f.request('start',{headers:{'X-Requested-With':''}})).status,403);assert.equal((await f.request('all')).status,401);assert.equal(f.puts.length,0);});
test('web authorization uses state, PKCE and server-only credentials; callback resumes once',async t=>{const f=await fixture(t),d=await f.start();assert.match(d.response.cookie,/HttpOnly; SameSite=Lax; Path=\/; Max-Age=900; Secure/);assert.equal(d.url.origin,'https://github.com');assert.equal(d.url.searchParams.get('code_challenge_method'),'S256');assert.ok(!JSON.stringify(d.response).includes('synthetic-secret'));const callback=await f.request('callback?state='+d.state+'&code=synthetic-code',{method:'GET'});assert.equal(callback.location,'/?star=authorized#projects');assert.ok(!JSON.stringify(callback).includes('synthetic-token'));assert.equal((await f.request('resume')).data.authorized,true);assert.equal((await f.request('resume')).data.authorized,false);assert.equal((await f.request('callback?state='+d.state+'&code=synthetic-code',{method:'GET'})).status,400);assert.ok((await f.request('all')).data.results.every(r=>r.ok));assert.deepEqual(f.puts,repos);await f.request('all');assert.equal(f.puts.length,repos.length);});
test('missing/wrong state or cookie does not exchange code; expired callback fails',async t=>{const f=await fixture(t),d=await f.start();for(const state of ['bad', '💠'.repeat(16),''])assert.equal((await f.request('callback?code=x&state='+encodeURIComponent(state),{method:'GET'})).status,400);assert.equal((await f.request('callback?code=x&state='+d.state,{method:'GET',headers:{Cookie:''}})).status,400);assert.equal(f.exchanges.length,0);f.advance(900001);assert.equal((await f.request('callback?code=x&state='+d.state,{method:'GET'})).status,400);});
test('denial or failed exchange never grants Star access',async t=>{const f=await fixture(t),d=await f.start();assert.equal((await f.request('callback?state='+d.state+'&error=access_denied',{method:'GET'})).location,'/?star=denied#projects');assert.equal((await f.request('all')).status,401);const g=await fixture(t,{exchangeError:true});assert.equal((await g.login()).location,'/?star=error#projects');assert.equal((await g.request('all')).status,401);});
test('partial retry skips success and rate limit stops the batch',async t=>{const f=await fixture(t,{status:n=>n===2?500:204});await f.login();assert.equal((await f.request('all')).data.results.filter(r=>r.ok).length,repos.length-1);await f.request('all');assert.equal(f.puts.length,repos.length+1);assert.equal(f.puts.at(-1),repos[1]);const g=await fixture(t,{status:403});await g.login();assert.ok((await g.request('all')).data.results.every(r=>!r.ok));assert.equal(g.puts.length,1);});
test('logout discards session and tokens',async t=>{const f=await fixture(t);await f.login();await f.request('logout');assert.equal((await f.request('all')).status,401);assert.equal(f.puts.length,0);});
test('local simulation completes with no GitHub traffic and is prohibited on public origins',async t=>{assert.throws(()=>createStarService({demo:true,origin}),/loopback/);const f=await fixture(t,{demo:true}),d=await f.start();assert.equal(d.url.pathname,'/api/stars/demo-consent');const consent=await f.request('demo-consent?state='+d.state,{method:'GET'});assert.match(consent.text,/不会修改真实 Star/);assert.equal((await f.request('callback?code=synthetic&state='+d.state,{method:'GET'})).status,303);const batch=await f.request('all');assert.equal(batch.data.demo,true);assert.ok(batch.data.results.every(r=>r.ok));assert.equal(f.exchanges.length+f.puts.length,0);});
13 changes: 13 additions & 0 deletions star-repositories.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[
"vllm-project/vllm-omni",
"vllm-project/afd-plugin",
"vllm-project/router",
"openJiuwen-ai/agent-infer",
"ThinkFlowLab/vllm-rlt",
"ThinkFlowLab/vllm-omni-mlx",
"InferMatrixLab/InferRSI",
"InferMatrixLab/qwen3-tts-engine",
"InferMatrixLab/drama-3-single-b300-engine",
"InferMatrixLab/minicpm-o-4.5-single-gpu-engine",
"InferMatrixLab/infermatrix-marketing"
]
Loading