Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a129bfd
Merge pull request #242 from TencentCloud/chore/sync-develop-after-ma…
github-actions[bot] Aug 12, 2026
e702c71
chore: fast pre-commit gate via pytest-testmon (change-scoped tests)
jubaoliang-tencent Aug 12, 2026
cdbeae2
fix(precommit): detect staged changes so testmon gate isn't a false g…
jubaoliang-tencent Aug 12, 2026
bf096ea
Merge pull request #244 from TencentCloud/chore/precommit-fast-gate
jubaoliang Aug 12, 2026
e62073a
feat(agents): allow sharing agents with other users
jubaoliang-tencent Aug 12, 2026
8cbb942
feat(experts): publish workspace snapshots as installable templates
jubaoliang-tencent Aug 12, 2026
d9a20b4
feat(auth): add OpenID Connect SSO login
jubaoliang-tencent Aug 12, 2026
9c55067
feat(models): add local ONNX embedding model runtime
jubaoliang-tencent Aug 12, 2026
e8a824b
feat(knowledge): add knowledge bases with chat retrieval
jubaoliang-tencent Aug 12, 2026
93afeef
fix(update): harden update status caching and store handling
jubaoliang-tencent Aug 12, 2026
1af14b8
chore: wire shared agents, SSO, experts, ONNX, and knowledge into the…
jubaoliang-tencent Aug 12, 2026
d332867
fix(models): make ONNX download detection work without fastembed
jubaoliang-tencent Aug 12, 2026
64403fd
feat: squash schema v5, HITL tool picker, runtime installs, KB/skills UX
jubaoliang-tencent Aug 13, 2026
cac9c6a
chore: update harness-browser dependency to version 0.7.5
liukewia Aug 13, 2026
75387c9
feat: add conversational skill manager
Aug 12, 2026
87025ed
fix: support SkillHub CLI on Windows
Aug 13, 2026
0b2cd63
test: stabilize agent reload concurrency check
Aug 13, 2026
fef73f4
fix: add audio mime types to media/preview allowlist (#245)
chujieHong Aug 12, 2026
02d1638
Feature/image polish (#263)
Bosheng0422 Aug 13, 2026
2b4528d
feat: add final-only channel response mode
Aug 13, 2026
a3091fc
chore: sync develop onto main after manual
github-actions[bot] Aug 13, 2026
d395ff0
Merge branch 'develop' into chore/sync-develop-after-manual
jubaoliang Aug 14, 2026
c055cc6
Merge pull request #267 from TencentCloud/chore/sync-develop-after-ma…
jubaoliang Aug 14, 2026
041628c
feat: per-user module permissions (RBAC) with admin bypass (#276)
jubaoliang Aug 14, 2026
3136f50
feat: automatic scheduled system backups (#280)
liukewia Aug 14, 2026
ed8324a
feat: expert publishing, thread fork, and dashboard UI improvements
jubaoliang-tencent Aug 14, 2026
07c606f
refactor: update BackupRestorePanel icon to CalendarClock
liukewia Aug 14, 2026
1990428
chore: add anti-spam guard for star-farming issues (#300)
liukewia Aug 14, 2026
8b15af4
feat: harden knowledge embedding setup and settings ACL
jubaoliang-tencent Aug 15, 2026
bb50ca1
chore: release 0.9.24
jubaoliang-tencent Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 5 additions & 3 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env bash
# Pre-commit gate: make all (format BE+FE + lint + typecheck + test) + dashboard build.
# Pre-commit gate: make precommit (format BE+FE + lint + typecheck + testmon-scoped
# tests) + dashboard build. The full suite still runs in CI (`make all`); this is a
# fast, change-aware local gate so commits don't wait on the whole test matrix.
# Enable once per clone: make install-hooks
# Bypass (emergency only): SKIP_PRECOMMIT=1 git commit ...
# Or: git commit --no-verify
Expand All @@ -20,8 +22,8 @@ while IFS= read -r line; do
[[ -n "$line" ]] && STAGED_FILES+=("$line")
done < <(git diff --cached --name-only --diff-filter=ACMR || true)

echo "[pre-commit] make all (format-all + lint + typecheck + test)"
make all
echo "[pre-commit] make precommit (format-all + lint + typecheck + testmon-scoped test)"
make precommit

# If format rewrote files that were already staged, refresh the index so the
# commit includes the formatted content (worktree vs index drift).
Expand Down
303 changes: 303 additions & 0 deletions .github/workflows/anti-spam-issues.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
name: Anti-spam issue guard

"on":
issues:
types: [opened]
workflow_dispatch:
inputs:
issue_number:
description: "Issue number to check manually, for example 19 or #19"
required: true
type: string

permissions:
contents: read
issues: write

jobs:
anti-spam:
runs-on: ubuntu-latest

steps:
- name: Close suspicious issues
uses: actions/github-script@v7
env:
LABEL_NAME: "pending-maintainer-review"
LABEL_COLOR: "cfd3d7"
AUTO_CLOSE: "true"
NEW_ACCOUNT_DAYS: "180"

with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;

let issue = context.payload.issue;

if (context.eventName === "workflow_dispatch") {
const rawIssueNumber = (
core.getInput("issue_number") ||
context.payload.inputs?.issue_number ||
""
).trim();

core.info(`Manual issue_number raw input: "${rawIssueNumber}"`);
core.info(`Workflow inputs payload: ${JSON.stringify(context.payload.inputs || {})}`);

const issueNumber = Number(rawIssueNumber.replace(/^#/, ""));

if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
core.setFailed(`Invalid issue_number: "${rawIssueNumber}"`);
return;
}

const issueResponse = await github.rest.issues.get({
owner,
repo,
issue_number: issueNumber
});

issue = issueResponse.data;
}

if (!issue || issue.pull_request) {
core.info("Not a normal issue, skipping.");
return;
}

const author = issue.user.login;
const association = issue.author_association || "NONE";

const trustedAssociations = new Set([
"OWNER",
"MEMBER",
"COLLABORATOR",
"CONTRIBUTOR"
]);

if (trustedAssociations.has(association)) {
core.info(`Trusted author association: ${association}. Skipping.`);
return;
}

if (issue.user.type === "Bot") {
core.info("Issue author is a bot. Skipping.");
return;
}

const userInfo = await github.rest.users.getByUsername({
username: author
});

const createdAt = new Date(userInfo.data.created_at);
const accountAgeDays = Math.floor((Date.now() - createdAt.getTime()) / 86400000);
const newAccountDays = Number(process.env.NEW_ACCOUNT_DAYS || 180);
const isNewAccount = accountAgeDays <= newAccountDays;

const issueText = `${issue.title || ""}\n${issue.body || ""}`;
const normalizedIssueText = issueText.replace(/\s+/g, " ");

function hit(patterns) {
return patterns.some(pattern => pattern.test(normalizedIssueText));
}

const strongStarFarmPatterns = [
/刷\s*星|买\s*星|假\s*星|增\s*星|刷\s*star|假\s*star/i,
/star\s*造假|star\s*作弊|star\s*作假|star\s*异常/i,
/star\s*farming|artificial\s*stars?|fake\s*stars?|bot\s*stars?/i,
/star\s*boost|star\s*boosting|boosting\s*service/i,
/bought\s*stars?|purchased\s*stars?|paid\s*stars?/i
];

const starContextPatterns = [
/\bstars?\b|starred|stargazers?/i,
/Star\s*数|Star\s*数量|Star\s*数据|Star\s*列表/i,
/星标|颗星|个星|这么多\s*Star/i,
/人气|热度/
];

const abnormalGrowthPatterns = [
/异常增长|突然增长|爆发式增长|快速增长|增长异常/i,
/暴增|飙升|猛增|激增|疯涨|刷到|涨到/i,
/过去\s*48\s*小时|48\s*小时|仅用了?\s*\d+\s*天|两天|2\s*天/i,
/新增了?\s*\d+|增加了?\s*\d+|从\s*\d+\s*飙升到\s*\d+/i,
/\d+\s*(颗|个)?\s*(Star|stars?|星)/i,
/创建时间集中|创建时间高度集中|注册时间高度集中/i
];

const suspiciousAccountPatterns = [
/注册日期集中|注册时间集中|创建日期集中|创建时间集中/i,
/集中在最近|最近\s*(两|2)\s*周|最近\s*\d+\s*天|7\s*天内|一周内/i,
/用户名像随机|随机字符串|随机生成/i,
/个人主页空空如也|主页空空如也|主页空|个人主页空/i,
/没有头像|无头像|没有仓库|无公开仓库|没有公开仓库/i,
/没有\s*followers?|没有\s*follower|无粉丝|零粉丝|no\s*followers?/i,
/空壳号|僵尸号|机器人账号|bot\s*accounts?/i,
/只\s*Star|只\s*star\s*不超过|Stargazer\s*列表|抽查了?\s*\d+\s*个账号/i
];

const reportThreatPatterns = [
/违规|举报|上报|报告给\s*GitHub|发往\s*GitHub/i,
/违规举报|举报到位|举报已递|举报已提交|举报交官方/i,
/违规流量|违规人气|清理人气|清除流量|流量清除/i,
/清零|清空|封禁|封禁项目|关闭项目|项目关停|项目关闭|项目下线/i,
/关停下线|入口关闭|归档备查|违规已定/i
];

const metricMismatchPatterns = [
/(npm|pip|下载量).{0,80}(Star|stars?|星)/i,
/(Star|stars?|星).{0,80}(npm|pip|下载量)/i,
/(Issue|PR|Fork|贡献者|社区活跃|活跃指标|月均).{0,80}(不匹配|太低|很低|对比|同类项目)/i,
/(同类项目|类似项目|对比关键指标|关键指标|指标).{0,80}(Star|stars?|星)/i,
/只有\s*Star\s*高|其他指标都远低于同类/i,
/真实用户|真实反馈|真实内容|真实评价/i
];

const reputationAttackPatterns = [
/诚信问题|诚信有问题|信任崩塌|失望|反面教材/i,
/简历|面试|候选人|找工作|pass\s*的|直接\s*pass/i,
/技术分享|教学案例|识别刷星项目/i,
/真实口碑|虚假热度|虚假的数字|真实用户的认可/i
];

const strongKeywordHit = hit(strongStarFarmPatterns);
const hasStarContext = hit(starContextPatterns);
const abnormalGrowthHit = hit(abnormalGrowthPatterns);
const suspiciousAccountHit = hit(suspiciousAccountPatterns);
const reportThreatHit = hit(reportThreatPatterns);
const metricMismatchHit = hit(metricMismatchPatterns);
const reputationAttackHit = hit(reputationAttackPatterns);

const starAccusationScore = [
abnormalGrowthHit,
suspiciousAccountHit,
reportThreatHit,
metricMismatchHit,
reputationAttackHit
].filter(Boolean).length;

const coordinatedThreatWithoutStarHit =
suspiciousAccountHit &&
reportThreatHit &&
/注册日期集中|注册时间集中|创建时间集中|最近\s*(两|2)\s*周|随机字符串|空壳号|无公开仓库|没有仓库|无粉丝|没有\s*followers?/i.test(normalizedIssueText);

const keywordHit =
strongKeywordHit ||
(hasStarContext && starAccusationScore >= 2) ||
coordinatedThreatWithoutStarHit;

let priorActivity = false;

try {
const authoredSearch = await github.rest.search.issuesAndPullRequests({
q: `repo:${owner}/${repo} author:${author}`,
per_page: 20
});

const priorAuthoredItems = authoredSearch.data.items.filter(
item => item.number !== issue.number
);

if (priorAuthoredItems.length > 0) {
priorActivity = true;
}
} catch (error) {
core.warning(`Failed to search authored issues/PRs: ${error.message}`);
}

try {
const commentedSearch = await github.rest.search.issuesAndPullRequests({
q: `repo:${owner}/${repo} commenter:${author}`,
per_page: 20
});

const priorCommentedItems = commentedSearch.data.items.filter(
item => item.number !== issue.number
);

if (priorCommentedItems.length > 0) {
priorActivity = true;
}
} catch (error) {
core.warning(`Failed to search commented issues/PRs: ${error.message}`);
}

const suspicious =
priorActivity === false &&
isNewAccount === true &&
keywordHit === true;

core.info(`Issue number: ${issue.number}`);
core.info(`Issue title: ${issue.title}`);
core.info(`Author: ${author}`);
core.info(`Association: ${association}`);
core.info(`Account age days: ${accountAgeDays}`);
core.info(`Is new account within ${newAccountDays} days: ${isNewAccount}`);
core.info(`Prior activity in repo: ${priorActivity}`);
core.info(`Strong keyword hit: ${strongKeywordHit}`);
core.info(`Has star context: ${hasStarContext}`);
core.info(`Abnormal growth hit: ${abnormalGrowthHit}`);
core.info(`Suspicious account hit: ${suspiciousAccountHit}`);
core.info(`Report/threat hit: ${reportThreatHit}`);
core.info(`Metric mismatch hit: ${metricMismatchHit}`);
core.info(`Reputation attack hit: ${reputationAttackHit}`);
core.info(`Star accusation score: ${starAccusationScore}`);
core.info(`Coordinated threat without star hit: ${coordinatedThreatWithoutStarHit}`);
core.info(`Final keyword hit: ${keywordHit}`);
core.info(`Suspicious: ${suspicious}`);

if (!suspicious) {
return;
}

const labelName = process.env.LABEL_NAME || "pending-maintainer-review";
const labelColor = process.env.LABEL_COLOR || "cfd3d7";
const autoClose = process.env.AUTO_CLOSE === "true";

try {
await github.rest.issues.createLabel({
owner,
repo,
name: labelName,
color: labelColor,
description: "Automatically closed by anti-spam guard; pending maintainer review"
});
} catch (error) {
if (error.status !== 422) {
core.warning(`Failed to create label: ${error.message}`);
}
}

await github.rest.issues.addLabels({
owner,
repo,
issue_number: issue.number,
labels: [labelName]
});

const commentBody = [
"👋 This issue was automatically closed by an anti-spam guard because it was opened by a recently created GitHub account with **no prior activity in this repository**, and the issue content matches a recent pattern of repetitive star-farming accusations.",
"",
"If you are a real user with a genuine report, sorry for the friction — just **leave a comment below** explaining your situation and a maintainer will reopen it.",
"",
"---",
"",
"本 Issue 由反刷屏自动规则关闭:提交账号为近期注册账号,且**从未与本仓库互动**,同时 Issue 内容匹配近期重复出现的刷星举报模式。如果你是真实用户,请在下方留言说明,维护者会重新开启。"
].join("\n");

await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: commentBody
});

if (autoClose) {
await github.rest.issues.update({
owner,
repo,
issue_number: issue.number,
state: "closed",
state_reason: "not_planned"
});
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ ENV/

# Test / coverage
.pytest_cache/
.testmondata*
.mypy_cache/
.ruff_cache/
.coverage
Expand Down Expand Up @@ -90,3 +91,4 @@ scripts/dev-local-link.sh
# Local agent planning docs (not for upstream)
docs/superpowers/
scripts/lh_image_install_octop_tencentos.sh
.worktrees/
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,44 @@

## [Unreleased]

## [0.9.24] - 2026-08-15

### 新增
- 知识库:新增知识库与对话检索,支持本地 ONNX 向量嵌入模型运行
- 认证:新增 OpenID Connect(OIDC)单点登录
- 权限:新增按用户模块权限(RBAC)及管理员绕过
- 专家:支持将工作区快照发布为可安装模板(专家市场)
- 智能体:支持将智能体共享给其他用户
- 技能:新增对话式技能管理器(SkillHub),并兼容 Windows
- 备份:新增自动定时系统备份
- 频道:新增 final-only 仅终稿回复模式
- 线程:支持会话线程分叉(fork)
- 体验:HITL 工具选择器、运行时按需安装、KB/技能 UX 优化;对话与镜像等界面打磨;知识嵌入初始化流程加固

### 修复
- 媒体/预览白名单补充音频 MIME 类型
- 修正 ONNX 下载检测在未安装 fastembed 时的误判
- 加固更新状态缓存与存储处理
- 预提交门控:修复 staged 变更检测,避免 testmon 门控误报为绿

### 变更
- 升级 harness-browser 依赖至 0.7.5
- 数据库 schema 收敛为 v5
- 备份恢复面板图标更新为 CalendarClock;对话/镜像等界面打磨

## [0.9.23] - 2026-08-13

### 修复
- 取消首次引导时删除 `octop-login.txt` 引导密码文件的逻辑,避免引导密码意外丢失
- 修复安装脚本版本显示问题,并将安装输出调整为英文

### 新增

- Octop-owned built-in `skill-manager` for conversational Skill lifecycle
management from uploaded files, archives, Git/GitHub or web URLs, and
SkillHub. It is seeded into every agent instance without modifying
harness-agent and installs user Skills only under that agent's `skills/`.

## [0.9.22] - 2026-08-11

### 新增
Expand Down
Loading
Loading