fix(游戏共创): 工程源码公开改成作者的主动知情确认,文案如实告知外发范围

背景(对抗性复核 D.7):工程源包的拒绝规则是**黑名单**,作者的 `docs/`、`*.pdf`、`notes.txt`、
截图、设计稿、会议记录等不在任何拒绝集内,会被原样外发。也就是说「授权非禁止 + 上传工程源码」
= **整个项目目录(除少数排除项)对任何登录用户公开**。原来那句「同时公开工程源码……取消勾选则
只能被试玩」是免责声明式措辞,不足以构成知情同意。

文案(最终措辞):
- 授权三态说明统一口径:「授权非「禁止共创」即允许他人改编(**没有单独的源码可见性开关**);
  是否随本次发布把工程源码一起交出去,由下面的确认决定。上架后授权只能单向提升,不能撤销。」
- 确认项(仅授权非「禁止共创」且首次发布时出现):
  「我已确认项目里没有不愿公开的内容,同意随本次发布公开**整个项目目录**:源码、素材与其中
  其它文件(含文档、截图、设计稿、笔记等)都会对任何登录用户公开;只会自动排除依赖目录、
  版本库、构建产物与常见凭据文件。一经发布**不可撤销**:即使之后把授权降级,也无法收回别人
  已经下载的内容。」
- 紧随其后写清不勾选的后果:「不勾选则本次不上传工程源码:作品仍可正常发布,但他人只能拿到
  已构建成品试玩与参考,不能直接重新发布。」

交互选择:**默认不勾选 + 不勾选就不上传**(不是禁用发布按钮)。
- 选它的理由:① 它把第 3 点做成了真正的「主动确认动作」;② 发布本身不被阻断——工程包缺失本来
  就不影响发布(服务端与原生链路都按「可选资产」设计),禁用按钮反而会把「禁止共创 / 只想先发
  出去」的作者一起挡住;③ 勾选状态只作用于本次版本上传,不引入平台侧的独立开关,授权语义不变。
- 注意:这一条**取代**了早先「授权非禁止时默认帮作者上传」的默认值——知情同意优先于默认帮忙;
  若要把默认值改回勾选,只需把 `projectPackageConfirmed` 初值改成 true 并翻转对应断言。

测试:改写两条面板用例——授权开放后确认项出现且**默认未勾选**、九处告知文案逐条断言(整个项目
目录 / 含文档截图设计稿笔记 / 只自动排除四类 / 不可撤销 / 无法收回已下载内容 / 不勾选则不上传 /
没有单独的源码可见性开关)、未确认时显式传 `includeProjectBundle: false`(不上传但照常发布)、
确认后不带该字段(由原生按授权执行上传)。
This commit is contained in:
2026-10-05 16:24:52 +08:00
parent 843ecbf4a2
commit 7972089a5f
2 changed files with 48 additions and 27 deletions
@@ -254,12 +254,13 @@ export function GameDistributionPublishPanel({
GAME_DISTRIBUTION_DEFAULT_FORK_AUTHORIZATION,
);
/**
* 是否随版本公开工程源包(默认勾选)。
* 作者是否已**主动确认**随本次发布公开整个项目目录(默认不勾选)。
*
* 口径(与产品一致):授权非「禁止共创」时默认帮作者把工程源码一起传上去,他人即可取走;
* 作者可以取消——取消后作品仍照常发布,只是退化为「只能被产物级改编(试玩 + 参考)」。
* 产品口径:授权非「禁止共创」即允许他人改编,但工程源码的拒绝规则是黑名单——作者的
* `docs/`、截图、设计稿、笔记等不在任何拒绝集内,会随包原样外发。所以这里不做「默认帮作者
* 上传」,而是要求作者确认后才能上传;不确认则作品照常发布,只是退化为「只能被产物级改编」。
*/
const [includeProjectBundle, setIncludeProjectBundle] = useState(true);
const [projectBundleConfirmed, setProjectBundleConfirmed] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const [result, setResult] = useState<GameDistributionPublishResult | null>(
@@ -772,9 +773,8 @@ export function GameDistributionPublishPanel({
publicationState === 'update'
? (publicationBinding?.revision ?? null)
: null,
// 只在「授权已开放但作者取消勾选」时显式关掉工程源包上传;其余情况由原生按
// 「授权非禁止即上传」判断,避免面板与原生各写一套默认值。
...(forkAuthorization !== 'forbidden' && !includeProjectBundle
// 只有作者主动确认后才让原生上传工程源码;未确认时显式关掉,作品照常发布。
...(forkAuthorization !== 'forbidden' && !projectBundleConfirmed
? { includeProjectBundle: false }
: {}),
});
@@ -971,27 +971,38 @@ export function GameDistributionPublishPanel({
<p className="game-distribution-publish-panel__fork-hint">
{forkAuthorizationLocked
? '共创授权只在首次发布时生效;上架后只能在网页端「我的作品」单向提升。'
: '开启后可被他人复刻改编(仅能拿到已构建成品,用于试玩与参考);上架后只能单向提升,不能撤销。'}
: '授权非「禁止共创」即允许他人改编(没有单独的源码可见性开关);是否随本次发布把工程源码一起交出去,由下面的确认决定。上架后授权只能单向提升,不能撤销。'}
</p>
{/*
工程源码是否随版本公开:授权非「禁止共创」时才有意义(禁止共创谁也取不走)。
默认勾选,且必须把「不可撤销」说清楚——源码一旦公开就无法收回。
工程源码外发是**知情同意**:工程源包的拒绝规则是黑名单,作者的 docs/、截图、
设计稿、笔记等不在任何拒绝集内,会随包一起对任何登录用户公开。因此这里不是
一句免责声明,而是必须由作者主动确认的动作(默认不勾选,不勾选就不上传源码)。
*/}
{forkAuthorization !== 'forbidden' && !forkAuthorizationLocked ? (
<label className="game-distribution-publish-panel__bundle-consent">
<input
type="checkbox"
checked={includeProjectBundle}
checked={projectBundleConfirmed}
onChange={(event) =>
setIncludeProjectBundle(event.target.checked)
setProjectBundleConfirmed(event.target.checked)
}
/>
<span>
同时公开工程源码:允许他人下载并使用你的工程源文件(含素材),
且不可撤销;取消勾选则本作品只能被他人试玩与参考(不能直接重新发布)
我已确认项目里没有不愿公开的内容,同意随本次发布公开
<strong>整个项目目录</strong>
:源码、素材与其中其它文件(含文档、截图、设计稿、笔记等)都会对任何登录用户公开;
只会自动排除依赖目录、版本库、构建产物与常见凭据文件。一经发布
<strong>不可撤销</strong>
:即使之后把授权降级,也无法收回别人已经下载的内容。
</span>
</label>
) : null}
{forkAuthorization !== 'forbidden' && !forkAuthorizationLocked ? (
<p className="game-distribution-publish-panel__fork-hint">
不勾选则本次不上传工程源码:作品仍可正常发布,但他人只能拿到已构建成品试玩与
参考,不能直接重新发布。
</p>
) : null}
</fieldset>
<label>
<span className="game-distribution-publish-panel__version-label">
@@ -382,7 +382,7 @@ describe('GameDistributionPublishPanel', () => {
expect(args?.metadata?.forkAuthorization).toBe('full');
});
test('授权开放后默认勾选公开工程源码,勾选时不重复传默认值', async () => {
test('授权开放后才出现源码公开确认,默认不勾选并如实告知后果', async () => {
installTauriInvoke(async () => undefined);
vi.mocked(publishLocalProjectGame).mockResolvedValue({
gameId: 'game_1',
@@ -395,31 +395,41 @@ describe('GameDistributionPublishPanel', () => {
});
renderPanel();
// 默认「禁止共创」时源码不会被任何人取走,因此不出现这个勾选。
// 默认「禁止共创」时源码不会被任何人取走,因此不出现这个确认。
expect(screen.queryByRole('checkbox')).toBeNull();
fireEvent.click(screen.getByRole('radio', { name: '允许非商用共创' }));
const consent = await screen.findByRole('checkbox');
// 默认勾选:授权非禁止时默认帮作者把工程源码一起传上去。
expect(consent).toHaveProperty('checked', true);
// 文案必须把「不可撤销」与「含素材」说清楚。
// 知情同意:必须由作者主动确认,默认不勾选。
expect(consent).toHaveProperty('checked', false);
// 告知必须如实:整个项目目录会外发、只自动排除少数几类、且不可撤销。
expect(screen.getByText(/我已确认项目里没有不愿公开的内容/u)).not.toBeNull();
expect(screen.getByText(/整个项目目录/u)).not.toBeNull();
expect(screen.getByText(/源码、素材与其中其它文件/u)).not.toBeNull();
expect(screen.getByText(/只会自动排除依赖目录、版本库、构建产物与常见凭据文件/u)).not.toBeNull();
expect(screen.getByText(/不可撤销/u)).not.toBeNull();
expect(screen.getByText(/无法收回别人已经下载的内容/u)).not.toBeNull();
// 不勾选的后果也要在发布前就说清楚。
expect(
screen.getByText(/允许他人下载并使用你的工程源文件(含素材)/u),
screen.getByText(/不勾选则本次不上传工程源码/u),
).not.toBeNull();
// 授权说明与确认动作同一口径:没有独立的源码可见性开关。
expect(
screen.getByText(/没有单独的源码可见性开关/u),
).not.toBeNull();
expect(screen.getByText(/且不可撤销/u)).not.toBeNull();
// 未确认 → 明确告诉原生不要把工程源码交出去(作品照常发布)。
await selectCover();
fireEvent.click(screen.getByRole('button', { name: '发布游戏' }));
await waitFor(() =>
expect(publishLocalProjectGame).toHaveBeenCalledTimes(1),
);
// 勾选状态留给原生按默认值判断:这里不带 includeProjectBundle。
expect(
vi.mocked(publishLocalProjectGame).mock.calls[0]?.[0],
).not.toHaveProperty('includeProjectBundle');
).toMatchObject({ includeProjectBundle: false });
});
test('取消勾选公开工程源码时显式传 includeProjectBundle=false', async () => {
test('确认公开整个项目目录后才上传工程源码', async () => {
installTauriInvoke(async () => undefined);
vi.mocked(publishLocalProjectGame).mockResolvedValue({
gameId: 'game_1',
@@ -435,7 +445,7 @@ describe('GameDistributionPublishPanel', () => {
fireEvent.click(screen.getByRole('radio', { name: '允许全开放共创' }));
fireEvent.click(await screen.findByRole('checkbox'));
await waitFor(() =>
expect(screen.queryByRole('checkbox')).toHaveProperty('checked', false),
expect(screen.queryByRole('checkbox')).toHaveProperty('checked', true),
);
await selectCover();
@@ -443,10 +453,10 @@ describe('GameDistributionPublishPanel', () => {
await waitFor(() =>
expect(publishLocalProjectGame).toHaveBeenCalledTimes(1),
);
// 显式 false:源码不外发,作品退化为产物级改编。
// 已确认时不带 includeProjectBundle:由原生按「授权非禁止」执行上传。
expect(
vi.mocked(publishLocalProjectGame).mock.calls[0]?.[0],
).toMatchObject({ includeProjectBundle: false });
).not.toHaveProperty('includeProjectBundle');
});
test('工程源包未上传时成功页如实提示只能产物级改编', async () => {